Bug 824433 - Bookmarks backup takes a long time to write out on shutdown. r=mano, a=sledru
authorMarco Bonardo <mbonardo@mozilla.com>
Tue, 18 Feb 2014 10:24:18 -0500
changeset 182890 09e19b83f5486cc464e0c57a30938566ee52ec69
parent 182889 f24272656ba229d2ae468a8e8b73d8c94fe45f83
child 182891 b8aab2d3e92ad721c0e243befac12b59e5342970
push id3343
push userffxbld
push dateMon, 17 Mar 2014 21:55:32 +0000
treeherdermozilla-beta@2f7d3415f79f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmano, sledru
bugs824433, 971686, 972434
milestone29.0a2
Bug 824433 - Bookmarks backup takes a long time to write out on shutdown. r=mano, a=sledru Includes regression fixes from Bug 971686 and Bug 972434
browser/app/profile/firefox.js
browser/components/nsBrowserGlue.js
browser/components/places/content/places.js
browser/components/places/tests/unit/test_421483.js
browser/components/places/tests/unit/test_browserGlue_shutdown.js
browser/components/places/tests/unit/test_clearHistory_shutdown.js
browser/components/places/tests/unit/xpcshell.ini
toolkit/components/places/BookmarkJSONUtils.jsm
toolkit/components/places/PlacesBackups.jsm
toolkit/components/places/tests/bookmarks/test_466303-json-remove-backups.js
toolkit/components/telemetry/Histograms.json
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -474,17 +474,17 @@ pref("browser.ctrlTab.recentlyUsedLimit"
 // If true, at shutdown the bookmarks in your menu and toolbar will
 // be exported as HTML to the bookmarks.html file.
 pref("browser.bookmarks.autoExportHTML",          false);
 
 // The maximum number of daily bookmark backups to 
 // keep in {PROFILEDIR}/bookmarkbackups. Special values:
 // -1: unlimited
 //  0: no backups created (and deletes all existing backups)
-pref("browser.bookmarks.max_backups",             10);
+pref("browser.bookmarks.max_backups",             15);
 
 // Scripts & Windows prefs
 pref("dom.disable_open_during_load",              true);
 pref("javascript.options.showInConsole",          true);
 #ifdef DEBUG
 pref("general.warnOnAboutConfig",                 false);
 #endif
 
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -77,26 +77,30 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/osfile.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
                                   "resource:///modules/sessionstore/SessionStore.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry",
                                   "resource:///modules/BrowserUITelemetry.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
+                                  "resource:///modules/AsyncShutdown.jsm");
+
 const PREF_PLUGINS_NOTIFYUSER = "plugins.update.notifyUser";
 const PREF_PLUGINS_UPDATEURL  = "plugins.update.url";
 
-// We try to backup bookmarks at idle times, to avoid doing that at shutdown.
-// Number of idle seconds before trying to backup bookmarks.  10 minutes.
-const BOOKMARKS_BACKUP_IDLE_TIME = 10 * 60;
-// Minimum interval in milliseconds between backups.
-const BOOKMARKS_BACKUP_INTERVAL = 86400 * 1000;
-// Maximum number of backups to create.  Old ones will be purged.
-const BOOKMARKS_BACKUP_MAX_BACKUPS = 10;
+// Seconds of idle before trying to create a bookmarks backup.
+const BOOKMARKS_BACKUP_IDLE_TIME_SEC = 10 * 60;
+// Minimum interval between backups.  We try to not create more than one backup
+// per interval.
+const BOOKMARKS_BACKUP_MIN_INTERVAL_DAYS = 1;
+// Maximum interval between backups.  If the last backup is older than these
+// days we will try to create a new one more aggressively.
+const BOOKMARKS_BACKUP_MAX_INTERVAL_DAYS = 5;
 
 // Factory object
 const BrowserGlueServiceFactory = {
   _instance: null,
   createInstance: function BGSF_createInstance(outer, iid) {
     if (outer != null)
       throw Components.results.NS_ERROR_NO_AGGREGATION;
     return this._instance == null ?
@@ -129,17 +133,16 @@ function BrowserGlue() {
 #ifndef XP_MACOSX
 # OS X has the concept of zero-window sessions and therefore ignores the
 # browser-lastwindow-close-* topics.
 #define OBSERVE_LASTWINDOW_CLOSE_TOPICS 1
 #endif
 
 BrowserGlue.prototype = {
   _saveSession: false,
-  _isIdleObserver: false,
   _isPlacesInitObserver: false,
   _isPlacesLockedObserver: false,
   _isPlacesShutdownObserver: false,
   _isPlacesDatabaseLocked: false,
   _migrationImportsDefaultBookmarks: false,
 
   _setPrefToSaveSession: function BG__setPrefToSaveSession(aForce) {
     if (!this._saveSession && !aForce)
@@ -257,18 +260,17 @@ BrowserGlue.prototype = {
         if (this._isPlacesShutdownObserver) {
           Services.obs.removeObserver(this, "places-shutdown");
           this._isPlacesShutdownObserver = false;
         }
         // places-shutdown is fired when the profile is about to disappear.
         this._onPlacesShutdown();
         break;
       case "idle":
-        if (this._idleService.idleTime > BOOKMARKS_BACKUP_IDLE_TIME * 1000)
-          this._backupBookmarks();
+        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;
         break;
       case "browser-glue-test": // used by tests
         if (data == "post-update-notification") {
@@ -417,18 +419,20 @@ BrowserGlue.prototype = {
     os.removeObserver(this, "browser-lastwindow-close-requested");
     os.removeObserver(this, "browser-lastwindow-close-granted");
 #endif
 #ifdef MOZ_SERVICES_SYNC
     os.removeObserver(this, "weave:service:ready");
     os.removeObserver(this, "weave:engine:clients:display-uri");
 #endif
     os.removeObserver(this, "session-save");
-    if (this._isIdleObserver)
-      this._idleService.removeIdleObserver(this, BOOKMARKS_BACKUP_IDLE_TIME);
+    if (this._bookmarksBackupIdleTime) {
+      this._idleService.removeIdleObserver(this, this._bookmarksBackupIdleTime);
+      delete this._bookmarksBackupIdleTime;
+    }
     if (this._isPlacesInitObserver)
       os.removeObserver(this, "places-init-complete");
     if (this._isPlacesLockedObserver)
       os.removeObserver(this, "places-database-locked");
     if (this._isPlacesShutdownObserver)
       os.removeObserver(this, "places-shutdown");
     os.removeObserver(this, "handle-xul-text-link");
     os.removeObserver(this, "profile-before-change");
@@ -1055,24 +1059,28 @@ BrowserGlue.prototype = {
           Services.prefs.getBoolPref("browser.bookmarks.restore_default_bookmarks");
         if (restoreDefaultBookmarks) {
           // Ensure that we already have a bookmarks backup for today.
           yield this._backupBookmarks();
           importBookmarks = true;
         }
       } catch(ex) {}
 
+      // This may be reused later, check for "=== undefined" to see if it has
+      // been populated already.
+      let lastBackupFile;
+
       // If the user did not require to restore default bookmarks, or import
       // from bookmarks.html, we will try to restore from JSON
       if (importBookmarks && !restoreDefaultBookmarks && !importBookmarksHTML) {
         // get latest JSON backup
-        var bookmarksBackupFile = yield PlacesBackups.getMostRecent("json");
-        if (bookmarksBackupFile) {
+        lastBackupFile = yield PlacesBackups.getMostRecentBackup("json");
+        if (lastBackupFile) {
           // restore from JSON backup
-          yield BookmarkJSONUtils.importFromFile(bookmarksBackupFile, true);
+          yield BookmarkJSONUtils.importFromFile(lastBackupFile, true);
           importBookmarks = false;
         }
         else {
           // We have created a new database but we don't have any backup available
           importBookmarks = true;
           var dirService = Cc["@mozilla.org/file/directory_service;1"].
                            getService(Ci.nsIProperties);
           var bookmarksHTMLFile = dirService.get("BMarks", Ci.nsILocalFile);
@@ -1157,100 +1165,95 @@ BrowserGlue.prototype = {
         if (importBookmarksHTML)
           Services.prefs.setBoolPref("browser.places.importBookmarksHTML", false);
         if (restoreDefaultBookmarks)
           Services.prefs.setBoolPref("browser.bookmarks.restore_default_bookmarks",
                                      false);
       }
 
       // Initialize bookmark archiving on idle.
-      // Once a day, either on idle or shutdown, bookmarks are backed up.
-      if (!this._isIdleObserver) {
-        this._idleService.addIdleObserver(this, BOOKMARKS_BACKUP_IDLE_TIME);
-        this._isIdleObserver = true;
+      if (!this._bookmarksBackupIdleTime) {
+        this._bookmarksBackupIdleTime = BOOKMARKS_BACKUP_IDLE_TIME_SEC;
+
+        // If there is no backup, or the last bookmarks backup is too old, use
+        // a more aggressive idle observer.
+        if (lastBackupFile === undefined)
+          lastBackupFile = yield PlacesBackups.getMostRecentBackup();
+        if (!lastBackupFile) {
+            this._bookmarksBackupIdleTime /= 2;
+        }
+        else {
+          let lastBackupTime = PlacesBackups.getDateForFile(lastBackupFile);
+          let profileLastUse = Services.appinfo.replacedLockTime || Date.now();
+
+          // If there is a backup after the last profile usage date it's fine,
+          // regardless its age.  Otherwise check how old is the last
+          // available backup compared to that session.
+          if (profileLastUse > lastBackupTime) {
+            let backupAge = Math.round((profileLastUse - lastBackupTime) / 86400000);
+            // Report the age of the last available backup.
+            try {
+              Services.telemetry
+                      .getHistogramById("PLACES_BACKUPS_DAYSFROMLAST")
+                      .add(backupAge);
+            } catch (ex) {
+              Components.utils.reportError("Unable to report telemetry.");
+            }
+
+            if (backupAge > BOOKMARKS_BACKUP_MAX_INTERVAL_DAYS)
+              this._bookmarksBackupIdleTime /= 2;
+          }
+        }
+        this._idleService.addIdleObserver(this, this._bookmarksBackupIdleTime);
       }
 
       Services.obs.notifyObservers(null, "places-browser-init-complete", "");
     }.bind(this));
   },
 
   /**
    * Places shut-down tasks
-   * - back up bookmarks if needed.
+   * - finalize components depending on Places.
    * - export bookmarks as HTML, if so configured.
-   * - finalize components depending on Places.
    */
   _onPlacesShutdown: function BG__onPlacesShutdown() {
     this._sanitizer.onShutdown();
     PageThumbs.uninit();
 
-    if (this._isIdleObserver) {
-      this._idleService.removeIdleObserver(this, BOOKMARKS_BACKUP_IDLE_TIME);
-      this._isIdleObserver = false;
+    if (this._bookmarksBackupIdleTime) {
+      this._idleService.removeIdleObserver(this, this._bookmarksBackupIdleTime);
+      delete this._bookmarksBackupIdleTime;
     }
 
-    let waitingForBackupToComplete = true;
-    this._backupBookmarks().then(
-      function onSuccess() {
-        waitingForBackupToComplete = false;
-      },
-      function onFailure() {
-        Cu.reportError("Unable to backup bookmarks.");
-        waitingForBackupToComplete = false;
+    // Support legacy bookmarks.html format for apps that depend on that format.
+    try {
+      if (Services.prefs.getBoolPref("browser.bookmarks.autoExportHTML")) {
+        // places-shutdown happens at profile-change-teardown, so here we
+        // can safely add a profile-before-change blocker.
+        AsyncShutdown.profileBeforeChange.addBlocker(
+          "Places: bookmarks.html",
+          () => BookmarkHTMLUtils.exportToFile(Services.dirsvc.get("BMarks", Ci.nsIFile))
+                                 .then(null, Cu.reportError)
+        );
       }
-    );
-
-    // Backup bookmarks to bookmarks.html to support apps that depend
-    // on the legacy format.
-    let waitingForHTMLExportToComplete = false;
-    // If this fails to get the preference value, we don't export.
-    if (Services.prefs.getBoolPref("browser.bookmarks.autoExportHTML")) {
-      // Exceptionally, since this is a non-default setting and HTML format is
-      // discouraged in favor of the JSON backups, we spin the event loop on
-      // shutdown, to wait for the export to finish.  We cannot safely spin
-      // the event loop on shutdown until we include a watchdog to prevent
-      // potential hangs (bug 518683).  The asynchronous shutdown operations
-      // will then be handled by a shutdown service (bug 435058).
-      waitingForHTMLExportToComplete = true;
-      BookmarkHTMLUtils.exportToFile(Services.dirsvc.get("BMarks", Ci.nsIFile)).then(
-        function onSuccess() {
-          waitingForHTMLExportToComplete = false;
-        },
-        function onFailure() {
-          Cu.reportError("Unable to auto export html.");
-          waitingForHTMLExportToComplete = false;
-        }
-      );
-    }
-
-    // The events loop should spin at least once because waitingForBackupToComplete
-    // is true before checking whether backup should be made.
-    let thread = Services.tm.currentThread;
-    while (waitingForBackupToComplete || waitingForHTMLExportToComplete) {
-      thread.processNextEvent(true);
-    }
+    } catch (ex) {} // Do not export.
   },
 
   /**
-   * Backup bookmarks.
+   * If a backup for today doesn't exist, this creates one.
    */
   _backupBookmarks: function BG__backupBookmarks() {
     return Task.spawn(function() {
       let lastBackupFile = yield PlacesBackups.getMostRecentBackup();
       // Should backup bookmarks if there are no backups or the maximum
       // interval between backups elapsed.
       if (!lastBackupFile ||
-          new Date() - PlacesBackups.getDateForFile(lastBackupFile) > BOOKMARKS_BACKUP_INTERVAL) {
-        let maxBackups = BOOKMARKS_BACKUP_MAX_BACKUPS;
-        try {
-          maxBackups = Services.prefs.getIntPref("browser.bookmarks.max_backups");
-        }
-        catch(ex) { /* Use default. */ }
-
-        yield PlacesBackups.create(maxBackups); // Don't force creation.
+          new Date() - PlacesBackups.getDateForFile(lastBackupFile) > BOOKMARKS_BACKUP_MIN_INTERVAL_DAYS * 86400000) {
+        let maxBackups = Services.prefs.getIntPref("browser.bookmarks.max_backups");
+        yield PlacesBackups.create(maxBackups);
       }
     });
   },
 
   /**
    * Show the notificationBox for a locked places database.
    */
   _showPlacesLockedNotificationBox: function BG__showPlacesLockedNotificationBox() {
--- a/browser/components/places/content/places.js
+++ b/browser/components/places/content/places.js
@@ -535,17 +535,18 @@ var PlacesOrganizer = {
    */
   backupBookmarks: function PO_backupBookmarks() {
     let dirSvc = Cc["@mozilla.org/file/directory_service;1"].
                  getService(Ci.nsIProperties);
     let backupsDir = dirSvc.get("Desk", Ci.nsILocalFile);
     let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
     let fpCallback = function fpCallback_done(aResult) {
       if (aResult != Ci.nsIFilePicker.returnCancel) {
-        PlacesBackups.saveBookmarksToJSONFile(fp.file);
+        // There is no OS.File version of the filepicker yet (Bug 937812).
+        PlacesBackups.saveBookmarksToJSONFile(fp.file.path);
       }
     };
 
     fp.init(window, PlacesUIUtils.getString("bookmarksBackupTitle"),
             Ci.nsIFilePicker.modeSave);
     fp.appendFilter(PlacesUIUtils.getString("bookmarksRestoreFilterName"),
                     PlacesUIUtils.getString("bookmarksRestoreFilterExtension"));
     fp.defaultString = PlacesBackups.getFilenameForDate();
--- a/browser/components/places/tests/unit/test_421483.js
+++ b/browser/components/places/tests/unit/test_421483.js
@@ -1,94 +1,81 @@
 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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/. */
 
-// Get bookmarks service
-try {
-  var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
-              getService(Ci.nsINavBookmarksService);
-} catch(ex) {
-  do_throw("Could not get Bookmarks service\n");
-}
-
-// Get annotation service
-try {
-  var annosvc = Cc["@mozilla.org/browser/annotation-service;1"].
-                getService(Ci.nsIAnnotationService);
-} catch(ex) {
-  do_throw("Could not get Annotation service\n");
-}
-
-// Get browser glue
-try {
-  var gluesvc = Cc["@mozilla.org/browser/browserglue;1"].
-                getService(Ci.nsIBrowserGlue).
-                QueryInterface(Ci.nsIObserver);
-  // Avoid default bookmarks import.
-  gluesvc.observe(null, "initial-migration-will-import-default-bookmarks", "");
-//  gluesvc.observe(null, "initial-migration-did-import-default-bookmarks", "");
-} catch(ex) {
-  do_throw("Could not get BrowserGlue service\n");
-}
-
-// Get pref service
-try {
-  var pref =  Cc["@mozilla.org/preferences-service;1"].
-              getService(Ci.nsIPrefBranch);
-} catch(ex) {
-  do_throw("Could not get Preferences service\n");
-}
 
 const SMART_BOOKMARKS_ANNO = "Places/SmartBookmark";
 const SMART_BOOKMARKS_PREF = "browser.places.smartBookmarksVersion";
 
-// main
+let gluesvc = Cc["@mozilla.org/browser/browserglue;1"].
+                getService(Ci.nsIBrowserGlue).
+                QueryInterface(Ci.nsIObserver);
+// Avoid default bookmarks import.
+gluesvc.observe(null, "initial-migration-will-import-default-bookmarks", "");
+
 function run_test() {
-  // TEST 1: smart bookmarks disabled
-  pref.setIntPref("browser.places.smartBookmarksVersion", -1);
-  gluesvc.ensurePlacesDefaultQueriesInitialized();
-  var smartBookmarkItemIds = annosvc.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
-  do_check_eq(smartBookmarkItemIds.length, 0);
-  // check that pref has not been bumped up
-  do_check_eq(pref.getIntPref("browser.places.smartBookmarksVersion"), -1);
+  run_next_test();
+}
 
-  // TEST 2: create smart bookmarks
-  pref.setIntPref("browser.places.smartBookmarksVersion", 0);
+add_task(function smart_bookmarks_disabled() {
+  Services.prefs.setIntPref("browser.places.smartBookmarksVersion", -1);
   gluesvc.ensurePlacesDefaultQueriesInitialized();
-  smartBookmarkItemIds = annosvc.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
-  do_check_neq(smartBookmarkItemIds.length, 0);
-  // check that pref has been bumped up
-  do_check_true(pref.getIntPref("browser.places.smartBookmarksVersion") > 0);
+  let smartBookmarkItemIds =
+    PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
+  do_check_eq(smartBookmarkItemIds.length, 0);
+  do_log_info("check that pref has not been bumped up");
+  do_check_eq(Services.prefs.getIntPref("browser.places.smartBookmarksVersion"), -1);
+});
 
-  var smartBookmarksCount = smartBookmarkItemIds.length;
-
-  // TEST 3: smart bookmarks restore
-  // remove one smart bookmark and restore
-  bmsvc.removeItem(smartBookmarkItemIds[0]);
-  pref.setIntPref("browser.places.smartBookmarksVersion", 0);
+add_task(function create_smart_bookmarks() {
+  Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0);
   gluesvc.ensurePlacesDefaultQueriesInitialized();
-  smartBookmarkItemIds = annosvc.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
-  do_check_eq(smartBookmarkItemIds.length, smartBookmarksCount);
-  // check that pref has been bumped up
-  do_check_true(pref.getIntPref("browser.places.smartBookmarksVersion") > 0);
+  let smartBookmarkItemIds =
+    PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
+  do_check_neq(smartBookmarkItemIds.length, 0);
+  do_log_info("check that pref has been bumped up");
+  do_check_true(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0);
+});
 
-  // TEST 4: move a smart bookmark, change its title, then restore
-  // smart bookmark should be restored in place
-  var parent = bmsvc.getFolderIdForItem(smartBookmarkItemIds[0]);
-  var oldTitle = bmsvc.getItemTitle(smartBookmarkItemIds[0]);
+add_task(function remove_smart_bookmark_and_restore() {
+  let smartBookmarkItemIds =
+    PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
+  let smartBookmarksCount = smartBookmarkItemIds.length;
+  do_log_info("remove one smart bookmark and restore");
+  PlacesUtils.bookmarks.removeItem(smartBookmarkItemIds[0]);
+  Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0);
+  gluesvc.ensurePlacesDefaultQueriesInitialized();
+  let smartBookmarkItemIds =
+    PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
+  do_check_eq(smartBookmarkItemIds.length, smartBookmarksCount);
+  do_log_info("check that pref has been bumped up");
+  do_check_true(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0);
+});
+
+add_task(function move_smart_bookmark_rename_and_restore() {
+  let smartBookmarkItemIds =
+    PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
+  let smartBookmarksCount = smartBookmarkItemIds.length;
+  do_log_info("smart bookmark should be restored in place");
+  let parent = PlacesUtils.bookmarks.getFolderIdForItem(smartBookmarkItemIds[0]);
+  let oldTitle = PlacesUtils.bookmarks.getItemTitle(smartBookmarkItemIds[0]);
   // create a subfolder and move inside it
-  var newParent = bmsvc.createFolder(parent, "test", bmsvc.DEFAULT_INDEX);
-  bmsvc.moveItem(smartBookmarkItemIds[0], newParent, bmsvc.DEFAULT_INDEX);
+  let newParent =
+    PlacesUtils.bookmarks.createFolder(parent, "test",
+                                       PlacesUtils.bookmarks.DEFAULT_INDEX);
+  PlacesUtils.bookmarks.moveItem(smartBookmarkItemIds[0], newParent,
+                                 PlacesUtils.bookmarks.DEFAULT_INDEX);
   // change title
-  bmsvc.setItemTitle(smartBookmarkItemIds[0], "new title");
+  PlacesUtils.bookmarks.setItemTitle(smartBookmarkItemIds[0], "new title");
   // restore
-  pref.setIntPref("browser.places.smartBookmarksVersion", 0);
+  Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0);
   gluesvc.ensurePlacesDefaultQueriesInitialized();
-  smartBookmarkItemIds = annosvc.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
+  smartBookmarkItemIds =
+    PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
   do_check_eq(smartBookmarkItemIds.length, smartBookmarksCount);
-  do_check_eq(bmsvc.getFolderIdForItem(smartBookmarkItemIds[0]), newParent);
-  do_check_eq(bmsvc.getItemTitle(smartBookmarkItemIds[0]), oldTitle);
-  // check that pref has been bumped up
-  do_check_true(pref.getIntPref("browser.places.smartBookmarksVersion") > 0);
-}
+  do_check_eq(PlacesUtils.bookmarks.getFolderIdForItem(smartBookmarkItemIds[0]), newParent);
+  do_check_eq(PlacesUtils.bookmarks.getItemTitle(smartBookmarkItemIds[0]), oldTitle);
+  do_log_info("check that pref has been bumped up");
+  do_check_true(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0);
+});
deleted file mode 100644
--- a/browser/components/places/tests/unit/test_browserGlue_shutdown.js
+++ /dev/null
@@ -1,153 +0,0 @@
-/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim:set ts=2 sw=2 sts=2 et: */
-/* 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/. */
-
-/**
- * Tests that nsBrowserGlue is correctly exporting based on preferences values,
- * and creating bookmarks backup if one does not exist for today.
- */
-
-// Initialize nsBrowserGlue after Places.
-let bg = Cc["@mozilla.org/browser/browserglue;1"].
-         getService(Ci.nsIBrowserGlue);
-
-// Initialize Places through Bookmarks Service.
-let bs = PlacesUtils.bookmarks;
-
-// Get other services.
-let ps = Services.prefs;
-let os = Services.obs;
-
-const PREF_AUTO_EXPORT_HTML = "browser.bookmarks.autoExportHTML";
-
-let tests = [];
-
-//------------------------------------------------------------------------------
-
-tests.push({
-  description: "Export to bookmarks.html if autoExportHTML is true.",
-  exec: function() {
-    remove_all_JSON_backups();
-
-    // Sanity check: we should have bookmarks on the toolbar.
-    do_check_true(bs.getIdForItemAt(bs.toolbarFolder, 0) > 0);
-
-    // Set preferences.
-    ps.setBoolPref(PREF_AUTO_EXPORT_HTML, true);
-
-    // Force nsBrowserGlue::_shutdownPlaces().
-    bg.QueryInterface(Ci.nsIObserver).observe(null,
-                                              PlacesUtils.TOPIC_SHUTDOWN,
-                                              null);
-
-    // Check bookmarks.html has been created.
-    check_bookmarks_html();
-    // Check JSON backup has been created.
-    check_JSON_backup(true);
-
-    // Check preferences have not been reverted.
-    do_check_true(ps.getBoolPref(PREF_AUTO_EXPORT_HTML));
-    // Reset preferences.
-    ps.setBoolPref(PREF_AUTO_EXPORT_HTML, false);
-
-    next_test();
-  }
-});
-
-//------------------------------------------------------------------------------
-
-tests.push({
-  description: "Export to bookmarks.html if autoExportHTML is true and a bookmarks.html exists.",
-  exec: function() {
-    // Sanity check: we should have bookmarks on the toolbar.
-    do_check_true(bs.getIdForItemAt(bs.toolbarFolder, 0) > 0);
-
-    // Set preferences.
-    ps.setBoolPref(PREF_AUTO_EXPORT_HTML, true);
-
-    // Create a bookmarks.html in the profile.
-    let profileBookmarksHTMLFile = create_bookmarks_html("bookmarks.glue.html");
-
-    // set the file's lastModifiedTime to one minute ago and get its size.
-    let lastMod = Date.now() - 60*1000;
-    profileBookmarksHTMLFile.lastModifiedTime = lastMod;
-
-    let fileSize = profileBookmarksHTMLFile.fileSize;
-
-    // Force nsBrowserGlue::_shutdownPlaces().
-    bg.QueryInterface(Ci.nsIObserver).observe(null,
-                                              PlacesUtils.TOPIC_SHUTDOWN,
-                                              null);
-
-    // Check a new bookmarks.html has been created.
-    let profileBookmarksHTMLFile = check_bookmarks_html();
-    do_check_true(profileBookmarksHTMLFile.lastModifiedTime > lastMod);
-    do_check_neq(profileBookmarksHTMLFile.fileSize, fileSize);
-
-    // Check preferences have not been reverted.
-    do_check_true(ps.getBoolPref(PREF_AUTO_EXPORT_HTML));
-    // Reset preferences.
-    ps.setBoolPref(PREF_AUTO_EXPORT_HTML, false);
-
-    next_test();
-  }
-});
-
-//------------------------------------------------------------------------------
-
-tests.push({
-  description: "Backup to JSON should be a no-op if a backup for today already exists.",
-  exec: function() {
-    // Sanity check: we should have bookmarks on the toolbar.
-    do_check_true(bs.getIdForItemAt(bs.toolbarFolder, 0) > 0);
-
-    // Create a JSON backup in the profile.
-    let profileBookmarksJSONFile = create_JSON_backup("bookmarks.glue.json");
-    // Get file lastModified and size.
-    let lastMod = profileBookmarksJSONFile.lastModifiedTime;
-    let fileSize = profileBookmarksJSONFile.fileSize;
-
-    // Force nsBrowserGlue::_shutdownPlaces().
-    bg.QueryInterface(Ci.nsIObserver).observe(null,
-                                              PlacesUtils.TOPIC_SHUTDOWN,
-                                              null);
-
-    // Check a new JSON backup has not been created.
-    do_check_true(profileBookmarksJSONFile.exists());
-    do_check_eq(profileBookmarksJSONFile.lastModifiedTime, lastMod);
-    do_check_eq(profileBookmarksJSONFile.fileSize, fileSize);
-
-    do_test_finished();
-  }
-});
-
-//------------------------------------------------------------------------------
-
-var testIndex = 0;
-function next_test() {
-  // Remove bookmarks.html from profile.
-  remove_bookmarks_html();
-
-  // Execute next test.
-  let test = tests.shift();
-  dump("\nTEST " + (++testIndex) + ": " + test.description);
-  test.exec();
-}
-
-function run_test() {
-  do_test_pending();
-
-  // Clean up bookmarks.
-  remove_all_bookmarks();
-
-  // Create some bookmarks.
-  bs.insertBookmark(bs.bookmarksMenuFolder, uri("http://mozilla.org/"),
-                    bs.DEFAULT_INDEX, "bookmark-on-menu");
-  bs.insertBookmark(bs.toolbarFolder, uri("http://mozilla.org/"),
-                    bs.DEFAULT_INDEX, "bookmark-on-toolbar");
-
-  // Kick-off tests.
-  next_test();
-}
--- a/browser/components/places/tests/unit/test_clearHistory_shutdown.js
+++ b/browser/components/places/tests/unit/test_clearHistory_shutdown.js
@@ -14,18 +14,18 @@ const URIS = [
 , "http://b.example2.com/"
 , "http://c.example3.com/"
 ];
 
 const TOPIC_CONNECTION_CLOSED = "places-connection-closed";
 
 let EXPECTED_NOTIFICATIONS = [
   "places-shutdown"
+, "places-will-close-connection"
 , "places-expiration-finished"
-, "places-will-close-connection"
 , "places-connection-closed"
 ];
 
 const UNEXPECTED_NOTIFICATIONS = [
   "xpcom-shutdown"
 ];
 
 const URL = "ftp://localhost/clearHistoryOnShutdown/";
--- a/browser/components/places/tests/unit/xpcshell.ini
+++ b/browser/components/places/tests/unit/xpcshell.ini
@@ -4,20 +4,20 @@ tail =
 firefox-appdir = browser
 support-files =
   bookmarks.glue.html
   bookmarks.glue.json
   corruptDB.sqlite
   distribution.ini
 
 [test_421483.js]
+skip-if = true # Bug 967839
 [test_browserGlue_corrupt.js]
 [test_browserGlue_corrupt_nobackup.js]
 [test_browserGlue_corrupt_nobackup_default.js]
 [test_browserGlue_distribution.js]
 [test_browserGlue_migrate.js]
 [test_browserGlue_prefs.js]
 [test_browserGlue_restore.js]
-[test_browserGlue_shutdown.js]
 [test_browserGlue_smartBookmarks.js]
 [test_clearHistory_shutdown.js]
 [test_leftpane_corruption_handling.js]
 [test_PUIU_makeTransaction.js]
--- a/toolkit/components/places/BookmarkJSONUtils.jsm
+++ b/toolkit/components/places/BookmarkJSONUtils.jsm
@@ -4,24 +4,34 @@
 
 this.EXPORTED_SYMBOLS = [ "BookmarkJSONUtils" ];
 
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
 const Cr = Components.results;
 
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
-Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
-Cu.import("resource://gre/modules/Sqlite.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
-Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
+  "resource://gre/modules/PlacesBackups.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
+  "resource://gre/modules/Deprecated.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "gTextDecoder", () => new TextDecoder());
+XPCOMUtils.defineLazyGetter(this, "gTextEncoder", () => new TextEncoder());
+XPCOMUtils.defineLazyGetter(this, "localFileCtor",
+  () => Components.Constructor("@mozilla.org/file/local;1",
+                               "nsILocalFile", "initWithPath"));
 
 this.BookmarkJSONUtils = Object.freeze({
   /**
    * Import bookmarks from a url.
    *
    * @param aURL
    *        url of the bookmark data.
    * @param aReplace
@@ -36,43 +46,71 @@ this.BookmarkJSONUtils = Object.freeze({
     return importer.importFromURL(aURL, aReplace);
   },
 
   /**
    * Restores bookmarks and tags from a JSON file.
    * @note any item annotated with "places/excludeFromBackup" won't be removed
    *       before executing the restore.
    *
-   * @param aFile
-   *        nsIFile of bookmarks in JSON format to be restored.
+   * @param aFilePath
+   *        OS.File path or nsIFile of bookmarks in JSON format to be restored.
    * @param aReplace
    *        Boolean if true, replace existing bookmarks, else merge.
    *
    * @return {Promise}
    * @resolves When the new bookmarks have been created.
    * @rejects JavaScript exception.
    */
-  importFromFile: function BJU_importFromFile(aFile, aReplace) {
+  importFromFile: function BJU_importFromFile(aFilePath, aReplace) {
     let importer = new BookmarkImporter();
-    return importer.importFromFile(aFile, aReplace);
+    // TODO (bug 967192): convert to pure OS.File
+    let file = aFilePath instanceof Ci.nsIFile ? aFilePath
+                                               : new localFileCtor(aFilePath);
+    return importer.importFromFile(file, aReplace);
   },
 
   /**
-   * Serializes bookmarks using JSON, and writes to the supplied file.
+   * Serializes bookmarks using JSON, and writes to the supplied file path.
    *
-   * @param aLocalFile
-   *        nsIFile for the "bookmarks.json" file to be created.
+   * @param aFilePath
+   *        OS.File path for the "bookmarks.json" file to be created.
    *
    * @return {Promise}
-   * @resolves When the file has been created.
+   * @resolves To the exported bookmarks count when the file has been created.
    * @rejects JavaScript exception.
+   * @deprecated passing an nsIFile is deprecated
    */
-  exportToFile: function BJU_exportToFile(aLocalFile) {
-    let exporter = new BookmarkExporter();
-    return exporter.exportToFile(aLocalFile);
+  exportToFile: function BJU_exportToFile(aFilePath) {
+    if (aFilePath instanceof Ci.nsIFile) {
+      Deprecated.warning("Passing an nsIFile to BookmarksJSONUtils.exportToFile " +
+                         "is deprecated. Please use an OS.File path instead.",
+                         "https://developer.mozilla.org/docs/JavaScript_OS.File");
+      aFilePath = aFilePath.path;
+    }
+    return Task.spawn(function* () {
+      let [bookmarks, count] = yield PlacesBackups.getBookmarksTree();
+      let startTime = Date.now();
+      let jsonString = JSON.stringify(bookmarks);
+      // Report the time taken to convert the tree to JSON.
+      try {
+        Services.telemetry
+                .getHistogramById("PLACES_BACKUPS_TOJSON_MS")
+                .add(Date.now() - startTime);
+      } catch (ex) {
+        Components.utils.reportError("Unable to report telemetry.");
+      }
+
+      // Do not write to the tmp folder, otherwise if it has a different
+      // filesystem writeAtomic will fail.  Eventual dangling .tmp files should
+      // be cleaned up by the caller.
+      yield OS.File.writeAtomic(aFilePath, jsonString,
+                                { tmpPath: OS.Path.join(aFilePath + ".tmp") });
+      return count;
+    });
   },
 
   /**
    * Takes a JSON-serialized node and inserts it into the db.
    *
    * @param aData
    *        The unwrapped data blob of dropped or pasted data.
    * @param aContainer
@@ -357,17 +395,18 @@ BookmarkImporter.prototype = {
                 PlacesUtils.tagging.tagURI(
                   NetUtil.newURI(aChild.uri), [aData.title]);
               } catch (ex) {
                 // Invalid tag child, skip it
               }
             });
             return [folderIdMap, searchIds];
           }
-        } else if (aData.livemark && aData.annos) {
+        } else if (aData.annos &&
+                   aData.annos.some(anno => anno.name == PlacesUtils.LMANNO_FEEDURI)) {
           // Node is a livemark
           let feedURI = null;
           let siteURI = null;
           aData.annos = aData.annos.filter(function(aAnno) {
             switch (aAnno.name) {
               case PlacesUtils.LMANNO_FEEDURI:
                 feedURI = NetUtil.newURI(aAnno.value);
                 return false;
@@ -417,17 +456,18 @@ BookmarkImporter.prototype = {
         }
         break;
       case PlacesUtils.TYPE_X_MOZ_PLACE:
         id = PlacesUtils.bookmarks.insertBookmark(
                aContainer, NetUtil.newURI(aData.uri), aIndex, aData.title);
         if (aData.keyword)
           PlacesUtils.bookmarks.setKeywordForBookmark(id, aData.keyword);
         if (aData.tags) {
-          let tags = aData.tags.split(", ");
+          // TODO (bug 967196) the tagging service should trim by itself.
+          let tags = aData.tags.split(",").map(tag => tag.trim());
           if (tags.length)
             PlacesUtils.tagging.tagURI(NetUtil.newURI(aData.uri), tags);
         }
         if (aData.charset) {
           PlacesUtils.annotations.setPageAnnotation(
             NetUtil.newURI(aData.uri), PlacesUtils.CHARSET_ANNO, aData.charset,
             0, Ci.nsIAnnotationService.EXPIRE_NEVER);
         }
@@ -497,316 +537,16 @@ function fixupQuery(aQueryURI, aFolderId
   let convert = function(str, p1, offset, s) {
     return "folder=" + aFolderIdMap[p1];
   }
   let stringURI = aQueryURI.spec.replace(/folder=([0-9]+)/g, convert);
 
   return NetUtil.newURI(stringURI);
 }
 
-function BookmarkExporter() {}
-BookmarkExporter.prototype = {
-  exportToFile: function BE_exportToFile(aLocalFile) {
-    return Task.spawn(this._writeToFile(aLocalFile));
-  },
-
-  _converterOut: null,
-
-  _writeToFile: function BE__writeToFile(aLocalFile) {
-    // Create a file that can be accessed by the current user only.
-    let safeFileOut = Cc["@mozilla.org/network/safe-file-output-stream;1"].
-                      createInstance(Ci.nsIFileOutputStream);
-    safeFileOut.init(aLocalFile, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
-                     FileUtils.MODE_TRUNCATE, parseInt("0600", 8), 0);
-    let nodeCount;
-
-    try {
-      // We need a buffered output stream for performance.  See bug 202477.
-      let bufferedOut = Cc["@mozilla.org/network/buffered-output-stream;1"].
-                        createInstance(Ci.nsIBufferedOutputStream);
-      bufferedOut.init(safeFileOut, 4096);
-      try {
-        // Write bookmarks in UTF-8.
-        this._converterOut = Cc["@mozilla.org/intl/converter-output-stream;1"].
-                             createInstance(Ci.nsIConverterOutputStream);
-        this._converterOut.init(bufferedOut, "utf-8", 0, 0);
-        try {
-          nodeCount = yield this._writeContentToFile();
-
-          // Flush the buffer and retain the target file on success only.
-          bufferedOut.QueryInterface(Ci.nsISafeOutputStream).finish();
-        } finally {
-          this._converterOut.close();
-          this._converterOut = null;
-        }
-      } finally {
-        bufferedOut.close();
-      }
-    } finally {
-      safeFileOut.close();
-    }
-    throw new Task.Result(nodeCount);
-  },
-
-  _writeContentToFile: function BE__writeContentToFile() {
-    return Task.spawn(function() {
-      // Weep over stream interface variance.
-      let streamProxy = {
-        converter: this._converterOut,
-        write: function(aData, aLen) {
-          this.converter.writeString(aData);
-        }
-      };
-
-      // Get list of itemIds that must be excluded from the backup.
-      let excludeItems = PlacesUtils.annotations.getItemsWithAnnotation(
-                           PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO);
-      // Serialize to JSON and write to stream.
-      let nodeCount = yield BookmarkRow.serializeJSONToOutputStream(streamProxy,
-                                                                    excludeItems);
-      throw new Task.Result(nodeCount);
-    }.bind(this));
-  }
-}
-
-let BookmarkRow = {
-  /**
-   * Serializes the SQL results as JSON with async SQL call and writes the
-   * serialization to the given output stream.
-   *
-   * @param   aStream
-   *          An nsIOutputStream. NOTE: it only uses the write(str, len)
-   *          method of nsIOutputStream. The caller is responsible for
-   *          closing the stream.
-   * @param   aExcludeItems
-   *          An array of item ids that should not be written to the backup.
-   * @return  {Promise}
-   * @resolves the number of serialized uri nodes.
-   */
-  serializeJSONToOutputStream: function(aStream, aExcludeItems) {
-    return Task.spawn(function() {
-      let nodes = [];
-      let nodeCount = 0;
-
-      let dbFilePath = OS.Path.join(OS.Constants.Path.profileDir,
-                                    "places.sqlite");
-      let conn = yield Sqlite.openConnection({ path: dbFilePath,
-                                               sharedMemoryCache: false });
-      try {
-        let rows = yield conn.execute(
-          "SELECT b.id, h.url, b.position, b.title, b.parent, " +
-            "b.type, b.dateAdded, b.lastModified, b.guid, t.parent AS grandParent " +
-          "FROM moz_bookmarks b " +
-          "LEFT JOIN moz_bookmarks t ON t.id = b.parent " +
-          "LEFT JOIN moz_places h ON h.id = b.fk " +
-          "ORDER BY b.parent, b.position, b.id");
-
-        // Create a Map for lookup.
-        let rowMap = new Map();
-        for (let row of rows) {
-          let parent = row.getResultByName("parent");
-          if (rowMap.has(parent)) {
-            let data = rowMap.get(parent);
-            data.children.push(row);
-          } else {
-            rowMap.set(parent, { children: [row] });
-          }
-        }
-
-        let root = rowMap.get(0);
-        if (!root) {
-          throw new Error("Root does not exist.");
-        }
-        let result = yield BookmarkRow._appendConvertedNode(root.children[0],
-                                                            rowMap,
-                                                            nodes,
-                                                            aExcludeItems);
-        if (result.appendedNode) {
-          nodeCount = result.nodeCount;
-          let json = JSON.stringify(nodes[0]);
-          aStream.write(json, json.length);
-        }
-      } catch(e) {
-        Cu.reportError("serializeJSONToOutputStream error " + e);
-      } finally {
-        yield conn.close();
-      }
-      throw new Task.Result(nodeCount);
-    });
-  },
-
-  _appendConvertedNode: function BR__appendConvertedNode(
-    aRow, aRowMap, aNodes, aExcludeItems) {
-    return Task.spawn(function() {
-      let node = {};
-      let nodeCount = 0;
-
-      this._addGenericProperties(aRow, node);
-
-      let parent = aRow.getResultByName("parent");
-      let grandParent = parent ? aRow.getResultByName("grandParent") : null;
-      let type = aRow.getResultByName("type");
-
-      if (type == Ci.nsINavBookmarksService.TYPE_BOOKMARK) {
-        // Tag root accept only folder nodes
-        if (parent == PlacesUtils.tagsFolderId)
-          throw new Task.Result({ appendedNode: false, nodeCount: nodeCount });
-
-        // Check for url validity, since we can't halt while writing a backup.
-        // This will throw if we try to serialize an invalid url and it does
-        // not make sense saving a wrong or corrupt uri node.
-        try {
-          NetUtil.newURI(aRow.getResultByName("url"));
-        } catch (ex) {
-          throw new Task.Result({ appendedNode: false, nodeCount: nodeCount });
-        }
-        yield this._addURIProperties(aRow, node);
-        nodeCount++;
-      } else if (type == Ci.nsINavBookmarksService.TYPE_FOLDER) {
-        // Tag containers accept only uri nodes
-        if (grandParent && grandParent == PlacesUtils.tagsFolderId) {
-          throw new Task.Result({ appendedNode: false, nodeCount: nodeCount });
-        }
-        this._addContainerProperties(aRow, node);
-      } else if (type == Ci.nsINavBookmarksService.TYPE_SEPARATOR) {
-        // Tag root accept only folder nodes
-        // Tag containers accept only uri nodes
-        if ((parent == PlacesUtils.tagsFolderId) ||
-            (grandParent == PlacesUtils.tagsFolderId)) {
-          throw new Task.Result({ appendedNode: false, nodeCount: nodeCount });
-        }
-        this._addSeparatorProperties(aRow, node);
-      }
-
-      if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) {
-        nodeCount += yield this._appendConvertedComplexNode(node,
-                                                            aNodes,
-                                                            aRowMap,
-                                                            aExcludeItems);
-        throw new Task.Result({ appendedNode: true, nodeCount: nodeCount });
-      }
-
-      aNodes.push(node);
-      throw new Task.Result({ appendedNode: true, nodeCount: nodeCount });
-    }.bind(this));
-  },
-
-  _addGenericProperties: function BR__addGenericProperties(aRow, aJSNode) {
-    let title = aRow.getResultByName("title")
-    aJSNode.title = title ? title : "";
-    aJSNode.guid = aRow.getResultByName("guid");
-    aJSNode.id = aRow.getResultByName("id");
-    aJSNode.index = aRow.getResultByName("position");
-    if (aJSNode.id != -1) {
-      let parent = aRow.getResultByName("parent");
-      if (parent)
-        aJSNode.parent = parent;
-      let dateAdded = aRow.getResultByName("dateAdded");;
-      if (dateAdded)
-        aJSNode.dateAdded = dateAdded;
-      let lastModified = aRow.getResultByName("lastModified");
-      if (lastModified)
-        aJSNode.lastModified = lastModified;
-
-      // XXX need a hasAnnos api
-      let annos = [];
-      try {
-        annos =
-          PlacesUtils.getAnnotationsForItem(aJSNode.id).filter(function(anno) {
-          // XXX should whitelist this instead, w/ a pref for
-          // backup/restore of non-whitelisted annos
-          // XXX causes JSON encoding errors, so utf-8 encode
-          // anno.value = unescape(encodeURIComponent(anno.value));
-          if (anno.name == PlacesUtils.LMANNO_FEEDURI)
-            aJSNode.livemark = 1;
-          return true;
-        });
-      } catch(ex) {}
-      if (annos.length != 0)
-        aJSNode.annos = annos;
-    }
-    // XXXdietrich - store annos for non-bookmark items
-  },
-
-  _addURIProperties: function BR__addURIProperties(aRow, aJSNode) {
-    return Task.spawn(function() {
-      aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE;
-      aJSNode.uri = aRow.getResultByName("url");
-      if (aJSNode.id && aJSNode.id != -1) {
-        // Harvest bookmark-specific properties
-        let keyword = PlacesUtils.bookmarks.getKeywordForBookmark(aJSNode.id);
-        if (keyword)
-          aJSNode.keyword = keyword;
-      }
-
-      // Last character-set
-      let uri = NetUtil.newURI(aRow.getResultByName("url"));
-      let lastCharset = yield PlacesUtils.getCharsetForURI(uri)
-      if (lastCharset)
-        aJSNode.charset = lastCharset;
-    });
-  },
-
-  _addSeparatorProperties: function BR__addSeparatorProperties(aRow, aJSNode) {
-    aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR;
-  },
-
-  _addContainerProperties: function BR__addContainerProperties(aRow, aJSNode) {
-    // This is a bookmark or a tag container.
-    // Bookmark folder or a shortcut we should convert to folder.
-    aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER;
-
-    // Mark root folders
-    let itemId = aRow.getResultByName("id");
-    if (itemId == PlacesUtils.placesRootId)
-      aJSNode.root = "placesRoot";
-    else if (itemId == PlacesUtils.bookmarksMenuFolderId)
-      aJSNode.root = "bookmarksMenuFolder";
-    else if (itemId == PlacesUtils.tagsFolderId)
-      aJSNode.root = "tagsFolder";
-    else if (itemId == PlacesUtils.unfiledBookmarksFolderId)
-      aJSNode.root = "unfiledBookmarksFolder";
-    else if (itemId == PlacesUtils.toolbarFolderId)
-      aJSNode.root = "toolbarFolder";
-  },
-
-  _appendConvertedComplexNode: function BR__appendConvertedComplexNode(
-    aNode, aNodes, aRowMap, aExcludeItems) {
-    return Task.spawn(function() {
-      let repr = {};
-      let nodeCount = 0;
-
-      for (let [name, value] in Iterator(aNode))
-        repr[name] = value;
-      repr.children = [];
-
-      let data = aRowMap.get(aNode.id);
-      if (data) {
-        for (let row of data.children) {
-          let id = row.getResultByName("id");
-          // ignore exclude items
-          if (aExcludeItems && aExcludeItems.indexOf(id) != -1) {
-            continue;
-          }
-          let result = yield this._appendConvertedNode(row,
-                                                       aRowMap,
-                                                       repr.children,
-                                                       aExcludeItems);
-          nodeCount += result.nodeCount;
-        }
-      } else {
-        Cu.reportError("_appendConvertedComplexNode error: Unable to find node");
-      }
-
-      aNodes.push(repr);
-      throw new Task.Result(nodeCount);
-    }.bind(this));
-  }
-}
-
 let BookmarkNode = {
   /**
    * Serializes the given node (and all its descendents) as JSON
    * and writes the serialization to the given output stream.
    *
    * @param   aNode
    *          An nsINavHistoryResultNode
    * @param   aStream
@@ -821,36 +561,36 @@ let BookmarkNode = {
    * @param   aExcludeItems
    *          An array of item ids that should not be written to the backup.
    * @returns Task promise
    * @resolves the number of serialized uri nodes.
    */
   serializeAsJSONToOutputStream: function BN_serializeAsJSONToOutputStream(
     aNode, aStream, aIsUICommand, aResolveShortcuts, aExcludeItems) {
 
-    return Task.spawn(function() {
+    return Task.spawn(function* () {
       // Serialize to stream
       let array = [];
       let result = yield this._appendConvertedNode(aNode, null, array,
                                                    aIsUICommand,
                                                    aResolveShortcuts,
                                                    aExcludeItems);
       if (result.appendedNode) {
-        let json = JSON.stringify(array[0]);
-        aStream.write(json, json.length);
+        let jsonString = JSON.stringify(array[0]);
+        aStream.write(jsonString, jsonString.length);
       } else {
         throw Cr.NS_ERROR_UNEXPECTED;
       }
-      throw new Task.Result(result.nodeCount);
+      return result.nodeCount;
     }.bind(this));
   },
 
   _appendConvertedNode: function BN__appendConvertedNode(
     bNode, aIndex, aArray, aIsUICommand, aResolveShortcuts, aExcludeItems) {
-    return Task.spawn(function() {
+    return Task.spawn(function* () {
       let node = {};
       let nodeCount = 0;
 
       // Set index in order received
       // XXX handy shortcut, but are there cases where we don't want
       // to export using the sorting provided by the query?
       if (aIndex)
         node.index = aIndex;
@@ -858,58 +598,58 @@ let BookmarkNode = {
       this._addGenericProperties(bNode, node, aResolveShortcuts);
 
       let parent = bNode.parent;
       let grandParent = parent ? parent.parent : null;
 
       if (PlacesUtils.nodeIsURI(bNode)) {
         // Tag root accept only folder nodes
         if (parent && parent.itemId == PlacesUtils.tagsFolderId)
-          throw new Task.Result({ appendedNode: false, nodeCount: nodeCount });
+          return { appendedNode: false, nodeCount: nodeCount };
 
         // Check for url validity, since we can't halt while writing a backup.
         // This will throw if we try to serialize an invalid url and it does
         // not make sense saving a wrong or corrupt uri node.
         try {
           NetUtil.newURI(bNode.uri);
         } catch (ex) {
-          throw new Task.Result({ appendedNode: false, nodeCount: nodeCount });
+          return { appendedNode: false, nodeCount: nodeCount };
         }
 
         yield this._addURIProperties(bNode, node, aIsUICommand);
         nodeCount++;
       } else if (PlacesUtils.nodeIsContainer(bNode)) {
         // Tag containers accept only uri nodes
         if (grandParent && grandParent.itemId == PlacesUtils.tagsFolderId)
-          throw new Task.Result({ appendedNode: false, nodeCount: nodeCount });
+          return { appendedNode: false, nodeCount: nodeCount };
 
         this._addContainerProperties(bNode, node, aIsUICommand,
                                      aResolveShortcuts);
       } else if (PlacesUtils.nodeIsSeparator(bNode)) {
         // Tag root accept only folder nodes
         // Tag containers accept only uri nodes
         if ((parent && parent.itemId == PlacesUtils.tagsFolderId) ||
             (grandParent && grandParent.itemId == PlacesUtils.tagsFolderId))
-          throw new Task.Result({ appendedNode: false, nodeCount: nodeCount });
+          return { appendedNode: false, nodeCount: nodeCount };
 
         this._addSeparatorProperties(bNode, node);
       }
 
       if (!node.feedURI && node.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) {
         nodeCount += yield this._appendConvertedComplexNode(node,
                                                            bNode,
                                                            aArray,
                                                            aIsUICommand,
                                                            aResolveShortcuts,
                                                            aExcludeItems)
-        throw new Task.Result({ appendedNode: true, nodeCount: nodeCount });
+        return { appendedNode: true, nodeCount: nodeCount };
       }
 
       aArray.push(node);
-      throw new Task.Result({ appendedNode: true, nodeCount: nodeCount });
+      return { appendedNode: true, nodeCount: nodeCount };
     }.bind(this));
   },
 
   _addGenericProperties: function BN__addGenericProperties(
     aPlacesNode, aJSNode, aResolveShortcuts) {
     aJSNode.title = aPlacesNode.title;
     aJSNode.id = aPlacesNode.itemId;
     if (aJSNode.id != -1) {
@@ -927,18 +667,16 @@ let BookmarkNode = {
       let annos = [];
       try {
         annos =
           PlacesUtils.getAnnotationsForItem(aJSNode.id).filter(function(anno) {
           // XXX should whitelist this instead, w/ a pref for
           // backup/restore of non-whitelisted annos
           // XXX causes JSON encoding errors, so utf-8 encode
           // anno.value = unescape(encodeURIComponent(anno.value));
-          if (anno.name == PlacesUtils.LMANNO_FEEDURI)
-            aJSNode.livemark = 1;
           if (anno.name == PlacesUtils.READ_ONLY_ANNO && aResolveShortcuts) {
             // When copying a read-only node, remove the read-only annotation.
             return false;
           }
           return true;
         });
       } catch(ex) {}
       if (annos.length != 0)
@@ -1009,26 +747,27 @@ let BookmarkNode = {
       aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE;
       aJSNode.uri = aPlacesNode.uri;
     }
   },
 
   _appendConvertedComplexNode: function BN__appendConvertedComplexNode(
     aNode, aSourceNode, aArray, aIsUICommand, aResolveShortcuts,
     aExcludeItems) {
-    return Task.spawn(function() {
+    return Task.spawn(function* () {
       let repr = {};
       let nodeCount = 0;
 
       for (let [name, value] in Iterator(aNode))
         repr[name] = value;
 
       // Write child nodes
       let children = repr.children = [];
-      if (!aNode.livemark) {
+      if (!aNode.annos ||
+          !aNode.annos.some(anno => anno.name == PlacesUtils.LMANNO_FEEDURI)) {
         PlacesUtils.asContainer(aSourceNode);
         let wasOpen = aSourceNode.containerOpen;
         if (!wasOpen)
           aSourceNode.containerOpen = true;
         let cc = aSourceNode.childCount;
         for (let i = 0; i < cc; ++i) {
           let childNode = aSourceNode.getChild(i);
           if (aExcludeItems && aExcludeItems.indexOf(childNode.itemId) != -1)
@@ -1038,12 +777,12 @@ let BookmarkNode = {
                                                        aExcludeItems);
           nodeCount += result.nodeCount;
         }
         if (!wasOpen)
           aSourceNode.containerOpen = false;
       }
 
       aArray.push(repr);
-      throw new Task.Result(nodeCount);
+      return nodeCount;
     }.bind(this));
   }
-}
\ No newline at end of file
+}
--- a/toolkit/components/places/PlacesBackups.jsm
+++ b/toolkit/components/places/PlacesBackups.jsm
@@ -12,21 +12,26 @@ const Cc = Components.classes;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
 Cu.import("resource://gre/modules/BookmarkJSONUtils.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/Deprecated.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
   "resource://gre/modules/osfile.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
-  "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
+  "resource://gre/modules/Sqlite.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "localFileCtor",
+  () => Components.Constructor("@mozilla.org/file/local;1",
+                               "nsILocalFile", "initWithPath"));
 
 this.PlacesBackups = {
   get _filenamesRegex() {
     // Get the localized backup filename, will be used to clear out
     // old backups with a localized name (bug 445704).
     let localizedFilename =
       PlacesUtils.getFormattedString("bookmarksArchiveFilename", [new Date()]);
     let localizedFilenamePrefix =
@@ -35,121 +40,136 @@ this.PlacesBackups = {
     return this._filenamesRegex =
       new RegExp("^(bookmarks|" + localizedFilenamePrefix + ")-([0-9-]+)(_[0-9]+)*\.(json|html)");
   },
 
   get folder() {
     Deprecated.warning(
       "PlacesBackups.folder is deprecated and will be removed in a future version",
       "https://bugzilla.mozilla.org/show_bug.cgi?id=859695");
+    return this._folder;
+  },
 
+  /**
+   * This exists just to avoid spamming deprecate warnings from internal calls
+   * needed to support deprecated methods themselves.
+   */
+  get _folder() {
     let bookmarksBackupDir = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
     bookmarksBackupDir.append(this.profileRelativeFolderPath);
     if (!bookmarksBackupDir.exists()) {
       bookmarksBackupDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0700", 8));
       if (!bookmarksBackupDir.exists())
         throw("Unable to create bookmarks backup folder");
     }
-    delete this.folder;
-    return this.folder = bookmarksBackupDir;
+    delete this._folder;
+    return this._folder = bookmarksBackupDir;
   },
 
   /**
    * Gets backup folder asynchronously.
    * @return {Promise}
    * @resolve the folder (the folder string path).
    */
   getBackupFolder: function PB_getBackupFolder() {
-    return Task.spawn(function() {
-      if (this._folder) {
-        throw new Task.Result(this._folder);
+    return Task.spawn(function* () {
+      if (this._backupFolder) {
+        return this._backupFolder;
       }
       let profileDir = OS.Constants.Path.profileDir;
       let backupsDirPath = OS.Path.join(profileDir, this.profileRelativeFolderPath);
-      yield OS.File.makeDir(backupsDirPath, { ignoreExisting: true }).then(
-        function onSuccess() {
-          this._folder = backupsDirPath;
-         }.bind(this),
-         function onError() {
-           throw("Unable to create bookmarks backup folder");
-         });
-       throw new Task.Result(this._folder);
-     }.bind(this));
+      yield OS.File.makeDir(backupsDirPath, { ignoreExisting: true });
+      return this._backupFolder = backupsDirPath;
+    }.bind(this));
   },
 
   get profileRelativeFolderPath() "bookmarkbackups",
 
   /**
    * Cache current backups in a sorted (by date DESC) array.
    */
   get entries() {
     Deprecated.warning(
       "PlacesBackups.entries is deprecated and will be removed in a future version",
       "https://bugzilla.mozilla.org/show_bug.cgi?id=859695");
+    return this._entries;
+  },
 
-    delete this.entries;
-    this.entries = [];
-    let files = this.folder.directoryEntries;
+  /**
+   * This exists just to avoid spamming deprecate warnings from internal calls
+   * needed to support deprecated methods themselves.
+   */
+  get _entries() {
+    delete this._entries;
+    this._entries = [];
+    let files = this._folder.directoryEntries;
     while (files.hasMoreElements()) {
       let entry = files.getNext().QueryInterface(Ci.nsIFile);
       // A valid backup is any file that matches either the localized or
       // not-localized filename (bug 445704).
       let matches = entry.leafName.match(this._filenamesRegex);
       if (!entry.isHidden() && matches) {
         // Remove bogus backups in future dates.
         if (this.getDateForFile(entry) > new Date()) {
           entry.remove(false);
           continue;
         }
-        this.entries.push(entry);
+        this._entries.push(entry);
       }
     }
-    this.entries.sort((a, b) => {
+    this._entries.sort((a, b) => {
       let aDate = this.getDateForFile(a);
       let bDate = this.getDateForFile(b);
       return aDate < bDate ? 1 : aDate > bDate ? -1 : 0;
     });
-    return this.entries;
+    return this._entries;
   },
 
   /**
    * Cache current backups in a sorted (by date DESC) array.
    * @return {Promise}
    * @resolve a sorted array of string paths.
    */
   getBackupFiles: function PB_getBackupFiles() {
-    return Task.spawn(function() {
-      if (this._backupFiles) {
-        throw new Task.Result(this._backupFiles);
-      }
+    return Task.spawn(function* () {
+      if (this._backupFiles)
+        return this._backupFiles;
+
       this._backupFiles = [];
 
       let backupFolderPath = yield this.getBackupFolder();
       let iterator = new OS.File.DirectoryIterator(backupFolderPath);
       yield iterator.forEach(function(aEntry) {
+        // Since this is a lazy getter and OS.File I/O is serialized, we can
+        // safely remove .tmp files without risking to remove ongoing backups.
+        if (aEntry.name.endsWith(".tmp")) {
+          OS.File.remove(aEntry.path);
+          return;
+        }
+
         let matches = aEntry.name.match(this._filenamesRegex);
         if (matches) {
           // Remove bogus backups in future dates.
           let filePath = aEntry.path;
           if (this.getDateForFile(filePath) > new Date()) {
             return OS.File.remove(filePath);
           } else {
             this._backupFiles.push(filePath);
           }
         }
       }.bind(this));
       iterator.close();
 
-      this._backupFiles.sort(function(a, b) {
+      this._backupFiles.sort((a, b) => {
         let aDate = this.getDateForFile(a);
         let bDate = this.getDateForFile(b);
         return aDate < bDate ? 1 : aDate > bDate ? -1 : 0;
-      }.bind(this));
+      });
 
-      throw new Task.Result(this._backupFiles);
+      return this._backupFiles;
     }.bind(this));
   },
 
   /**
    * Creates a filename for bookmarks backup files.
    *
    * @param [optional] aDateObj
    *                   Date object used to build the filename.
@@ -189,97 +209,101 @@ this.PlacesBackups = {
    * @returns nsIFile backup file
    */
   getMostRecent: function PB_getMostRecent(aFileExt) {
     Deprecated.warning(
       "PlacesBackups.getMostRecent is deprecated and will be removed in a future version",
       "https://bugzilla.mozilla.org/show_bug.cgi?id=859695");
 
     let fileExt = aFileExt || "(json|html)";
-    for (let i = 0; i < this.entries.length; i++) {
+    for (let i = 0; i < this._entries.length; i++) {
       let rx = new RegExp("\." + fileExt + "$");
-      if (this.entries[i].leafName.match(rx))
-        return this.entries[i];
+      if (this._entries[i].leafName.match(rx))
+        return this._entries[i];
     }
     return null;
   },
 
    /**
     * Get the most recent backup file.
     *
     * @param [optional] aFileExt
     *                   Force file extension.  Either "html" or "json".
     *                   Will check for both if not defined.
     * @return {Promise}
     * @result the path to the file.
     */
    getMostRecentBackup: function PB_getMostRecentBackup(aFileExt) {
-     return Task.spawn(function() {
+     return Task.spawn(function* () {
        let fileExt = aFileExt || "(json|html)";
        let entries = yield this.getBackupFiles();
        for (let entry of entries) {
          let rx = new RegExp("\." + fileExt + "$");
          if (OS.Path.basename(entry).match(rx)) {
-           throw new Task.Result(entry);
+           return entry;
          }
        }
-       throw new Task.Result(null);
+       return null;
     }.bind(this));
   },
 
   /**
    * Serializes bookmarks using JSON, and writes to the supplied file.
    * Note: any item that should not be backed up must be annotated with
    *       "places/excludeFromBackup".
    *
-   * @param aFile
-   *        nsIFile where to save JSON backup.
+   * @param aFilePath
+   *        OS.File path for the "bookmarks.json" file to be created.
    * @return {Promise}
    * @resolves the number of serialized uri nodes.
+   * @deprecated passing an nsIFile is deprecated
    */
-  saveBookmarksToJSONFile: function PB_saveBookmarksToJSONFile(aFile) {
-    return Task.spawn(function() {
-      let nodeCount = yield BookmarkJSONUtils.exportToFile(aFile);
+  saveBookmarksToJSONFile: function PB_saveBookmarksToJSONFile(aFilePath) {
+    if (aFilePath instanceof Ci.nsIFile) {
+      Deprecated.warning("Passing an nsIFile to PlacesBackups.saveBookmarksToJSONFile " +
+                         "is deprecated. Please use an OS.File path instead.",
+                         "https://developer.mozilla.org/docs/JavaScript_OS.File");
+      aFilePath = aFilePath.path;
+    }
+    return Task.spawn(function* () {
+      let nodeCount = yield BookmarkJSONUtils.exportToFile(aFilePath);
 
       let backupFolderPath = yield this.getBackupFolder();
-      if (aFile.parent.path == backupFolderPath) {
-        // Update internal cache.
-        this.entries.push(aFile);
+      if (OS.Path.dirname(aFilePath) == backupFolderPath) {
+        // We are creating a backup in the default backups folder,
+        // so just update the internal cache.
+        this._entries.unshift(new localFileCtor(aFilePath));
         if (!this._backupFiles) {
           yield this.getBackupFiles();
         }
-        this._backupFiles.push(aFile.path);
+        this._backupFiles.unshift(aFilePath);
       } else {
         // If we are saving to a folder different than our backups folder, then
         // we also want to copy this new backup to it.
         // This way we ensure the latest valid backup is the same saved by the
         // user.  See bug 424389.
         let name = this.getFilenameForDate();
         let newFilename = this._appendMetaDataToFilename(name,
                                                          { nodeCount: nodeCount });
         let newFilePath = OS.Path.join(backupFolderPath, newFilename);
         let backupFile = yield this._getBackupFileForSameDate(name);
-
-        if (backupFile) {
-          yield OS.File.remove(backupFile, { ignoreAbsent: true });
-        } else {
-          let file = new FileUtils.File(newFilePath);
-
+        if (!backupFile) {
           // Update internal cache if we are not replacing an existing
           // backup file.
-          this.entries.push(file);
+          this._entries.unshift(new localFileCtor(newFilePath));
           if (!this._backupFiles) {
             yield this.getBackupFiles();
           }
-          this._backupFiles.push(file.path);
+          this._backupFiles.unshift(newFilePath);
         }
-        yield OS.File.copy(aFile.path, newFilePath);
+
+        yield OS.File.copy(aFilePath, newFilePath);
       }
 
-      throw new Task.Result(nodeCount);
+      return nodeCount;
     }.bind(this));
   },
 
   /**
    * Creates a dated backup in <profile>/bookmarkbackups.
    * Stores the bookmarks using JSON.
    * Note: any item that should not be backed up must be annotated with
    *       "places/excludeFromBackup".
@@ -287,42 +311,38 @@ this.PlacesBackups = {
    * @param [optional] int aMaxBackups
    *                       The maximum number of backups to keep.
    * @param [optional] bool aForceBackup
    *                        Forces creating a backup even if one was already
    *                        created that day (overwrites).
    * @return {Promise}
    */
   create: function PB_create(aMaxBackups, aForceBackup) {
-    return Task.spawn(function() {
+    return Task.spawn(function* () {
       // Construct the new leafname.
       let newBackupFilename = this.getFilenameForDate();
       let mostRecentBackupFile = yield this.getMostRecentBackup();
 
       if (!aForceBackup) {
-        let numberOfBackupsToDelete = 0;
-        if (aMaxBackups !== undefined && aMaxBackups > -1) {
-          let backupFiles = yield this.getBackupFiles();
-          numberOfBackupsToDelete = backupFiles.length - aMaxBackups;
-        }
+        let backupFiles = yield this.getBackupFiles();
+        // If there are backups, limit them to aMaxBackups, if requested.
+        if (backupFiles.length > 0 && typeof aMaxBackups == "number" &&
+            aMaxBackups > -1 && backupFiles.length >= aMaxBackups) {
+          let numberOfBackupsToDelete = backupFiles.length - aMaxBackups;
 
-        if (numberOfBackupsToDelete > 0) {
           // If we don't have today's backup, remove one more so that
           // the total backups after this operation does not exceed the
           // number specified in the pref.
-          if (!mostRecentBackupFile ||
-              !this._isFilenameWithSameDate(OS.Path.basename(mostRecentBackupFile),
-                                            newBackupFilename))
+          if (!this._isFilenameWithSameDate(OS.Path.basename(mostRecentBackupFile),
+                                            newBackupFilename)) {
             numberOfBackupsToDelete++;
+          }
 
           while (numberOfBackupsToDelete--) {
-            this.entries.pop();
-            if (!this._backupFiles) {
-              yield this.getBackupFiles();
-            }
+            this._entries.pop();
             let oldestBackup = this._backupFiles.pop();
             yield OS.File.remove(oldestBackup);
           }
         }
 
         // Do nothing if we already have this backup or we don't want backups.
         if (aMaxBackups === 0 ||
             (mostRecentBackupFile &&
@@ -336,35 +356,32 @@ this.PlacesBackups = {
         if (aForceBackup) {
           yield OS.File.remove(backupFile, { ignoreAbsent: true });
         } else {
           return;
         }
       }
 
       // Save bookmarks to a backup file.
-      let backupFolderPath = yield this.getBackupFolder();
-      let backupFolder = new FileUtils.File(backupFolderPath);
-      let newBackupFile = backupFolder.clone();
-      newBackupFile.append(newBackupFilename);
-
+      let backupFolder = yield this.getBackupFolder();
+      let newBackupFile = OS.Path.join(backupFolder, newBackupFilename);
       let nodeCount = yield this.saveBookmarksToJSONFile(newBackupFile);
       // Rename the filename with metadata.
       let newFilenameWithMetaData = this._appendMetaDataToFilename(
                                       newBackupFilename,
                                       { nodeCount: nodeCount });
-      newBackupFile.moveTo(backupFolder, newFilenameWithMetaData);
+      let newBackupFileWithMetadata = OS.Path.join(backupFolder, newFilenameWithMetaData);
+      yield OS.File.move(newBackupFile, newBackupFileWithMetadata);
 
       // Update internal cache.
-      let newFileWithMetaData = backupFolder.clone();
-      newFileWithMetaData.append(newFilenameWithMetaData);
-      this.entries.pop();
-      this.entries.push(newFileWithMetaData);
+      let newFileWithMetaData = new localFileCtor(newBackupFileWithMetadata);
+      this._entries.pop();
+      this._entries.unshift(newFileWithMetaData);
       this._backupFiles.pop();
-      this._backupFiles.push(newFileWithMetaData.path);
+      this._backupFiles.unshift(newBackupFileWithMetadata);
     }.bind(this));
   },
 
   _appendMetaDataToFilename:
   function PB__appendMetaDataToFilename(aFilename, aMetaData) {
     let matches = aFilename.match(this._filenamesRegex);
     let newFilename = matches[1] + "-" + matches[2] + "_" +
                       aMetaData.nodeCount + "." + matches[4];
@@ -395,27 +412,221 @@ this.PlacesBackups = {
     let targetMatches = aTargetName.match(this._filenamesRegex);
 
     return (sourceMatches && targetMatches &&
             sourceMatches[1] == targetMatches[1] &&
             sourceMatches[2] == targetMatches[2] &&
             sourceMatches[4] == targetMatches[4]);
     },
 
-   _getBackupFileForSameDate:
-   function PB__getBackupFileForSameDate(aFilename) {
-     return Task.spawn(function() {
-       let backupFolderPath = yield this.getBackupFolder();
-       let iterator = new OS.File.DirectoryIterator(backupFolderPath);
-       let backupFile;
+  _getBackupFileForSameDate:
+  function PB__getBackupFileForSameDate(aFilename) {
+    return Task.spawn(function* () {
+      let backupFolderPath = yield this.getBackupFolder();
+      let iterator = new OS.File.DirectoryIterator(backupFolderPath);
+      let backupFile;
+
+      yield iterator.forEach(function(aEntry) {
+        if (this._isFilenameWithSameDate(aEntry.name, aFilename)) {
+          backupFile = aEntry.path;
+          return iterator.close();
+        }
+      }.bind(this));
+      yield iterator.close();
+
+      return backupFile;
+    }.bind(this));
+  },
+
+  /**
+   * Gets a bookmarks tree representation usable to create backups in different
+   * file formats.  The root or the tree is PlacesUtils.placesRootId.
+   * Items annotated with PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO and all of their
+   * descendants are excluded.
+   *
+   * @return an object representing a tree with the places root as its root.
+   *         Each bookmark is represented by an object having these properties:
+   *         * id: the item id (make this not enumerable after bug 824502)
+   *         * title: the title
+   *         * guid: unique id
+   *         * parent: item id of the parent folder, not enumerable
+   *         * index: the position in the parent
+   *         * dateAdded: microseconds from the epoch
+   *         * lastModified: microseconds from the epoch
+   *         * type: type of the originating node as defined in PlacesUtils 
+   *         The following properties exist only for a subset of bookmarks:
+   *         * annos: array of annotations
+   *         * uri: url
+   *         * keyword: associated keyword
+   *         * charset: last known charset
+   *         * tags: csv string of tags
+   *         * root: string describing whether this represents a root
+   *         * children: array of child items in a folder
+   */
+  getBookmarksTree: function () {
+    return Task.spawn(function* () {
+      let dbFilePath = OS.Path.join(OS.Constants.Path.profileDir, "places.sqlite");
+      let conn = yield Sqlite.openConnection({ path: dbFilePath,
+                                               sharedMemoryCache: false });
+      let rows = [];
+      try {
+        rows = yield conn.execute(
+          "SELECT b.id, h.url, IFNULL(b.title, '') AS title, b.parent, " +
+                 "b.position AS [index], b.type, b.dateAdded, b.lastModified, b.guid, " +
+                 "( SELECT GROUP_CONCAT(t.title, ',') " +
+                   "FROM moz_bookmarks b2 " +
+                   "JOIN moz_bookmarks t ON t.id = +b2.parent AND t.parent = :tags_folder " +
+                   "WHERE b2.fk = h.id " +
+                 ") AS tags, " +
+                 "EXISTS (SELECT 1 FROM moz_items_annos WHERE item_id = b.id LIMIT 1) AS has_annos, " +
+                 "( SELECT a.content FROM moz_annos a " +
+                   "JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id " +
+                   "WHERE place_id = h.id AND n.name = :charset_anno " +
+                 ") AS charset " +
+          "FROM moz_bookmarks b " +
+          "LEFT JOIN moz_bookmarks p ON p.id = b.parent " +
+          "LEFT JOIN moz_places h ON h.id = b.fk " +
+          "WHERE b.id <> :tags_folder AND b.parent <> :tags_folder AND p.parent <> :tags_folder " +
+          "ORDER BY b.parent, b.position",
+          { tags_folder: PlacesUtils.tagsFolderId,
+            charset_anno: PlacesUtils.CHARSET_ANNO });
+      } catch(e) {
+        Cu.reportError("Unable to query the database " + e);
+      } finally {
+        yield conn.close();
+      }
+
+      let startTime = Date.now();
+      // Create a Map for lookup and recursive building of the tree.
+      let itemsMap = new Map();
+      for (let row of rows) {
+        let id = row.getResultByName("id");
+        try {
+          let bookmark = sqliteRowToBookmarkObject(row);
+          if (itemsMap.has(id)) {
+            // Since children may be added before parents, we should merge with
+            // the existing object.
+            let original = itemsMap.get(id);
+            for (prop in bookmark) {
+              original[prop] = bookmark[prop];
+            }
+            bookmark = original;
+          }
+          else {
+            itemsMap.set(id, bookmark);
+          }
 
-       yield iterator.forEach(function(aEntry) {
-         if (this._isFilenameWithSameDate(aEntry.name, aFilename)) {
-           backupFile = aEntry.path;
-           return iterator.close();
-         }
-       }.bind(this));
-       yield iterator.close();
+          // Append bookmark to its parent.
+          if (!itemsMap.has(bookmark.parent))
+            itemsMap.set(bookmark.parent, {});
+          let parent = itemsMap.get(bookmark.parent);
+          if (!("children" in parent))
+            parent.children = [];
+          parent.children.push(bookmark);
+        } catch (e) {
+          Cu.reportError("Error while reading node " + id + " " + e);
+        }
+      }
+
+      // Handle excluded items, by removing entire subtrees pointed by them.
+      function removeFromMap(id) {
+        // Could have been removed by a previous call, since we can't
+        // predict order of items in EXCLUDE_FROM_BACKUP_ANNO.
+        if (itemsMap.has(id)) {
+          let excludedItem = itemsMap.get(id);
+          if (excludedItem.children) {
+            for (let child of excludedItem.children) {
+              removeFromMap(child.id);
+            }
+          }
+          // Remove the excluded item from its parent's children...
+          let parentItem = itemsMap.get(excludedItem.parent);
+          parentItem.children = parentItem.children.filter(aChild => aChild.id != id);
+          // ...then remove it from the map.
+          itemsMap.delete(id);
+        }
+      }
+
+      for (let id of PlacesUtils.annotations.getItemsWithAnnotation(
+                       PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO)) {
+        removeFromMap(id);
+      }
+
+      // Report the time taken to build the tree. This doesn't take into
+      // account the time spent in the query since that's off the main-thread.
+      try {
+        Services.telemetry
+                .getHistogramById("PLACES_BACKUPS_BOOKMARKSTREE_MS")
+                .add(Date.now() - startTime);
+      } catch (ex) {
+        Components.utils.reportError("Unable to report telemetry.");
+      }
+
+      return [itemsMap.get(PlacesUtils.placesRootId), itemsMap.size];
+    });
+  }
+}
 
-       throw new Task.Result(backupFile);
-     }.bind(this));
-   }
+/**
+ * Helper function to convert a Sqlite.jsm row to a bookmark object
+ * representation.
+ *
+ * @param aRow The Sqlite.jsm result row.
+ */
+function sqliteRowToBookmarkObject(aRow) {
+  let bookmark = {};
+  for (let p of [ "id" ,"guid", "title", "index", "dateAdded", "lastModified" ]) {
+    bookmark[p] = aRow.getResultByName(p);
+  }
+  Object.defineProperty(bookmark, "parent",
+                        { value: aRow.getResultByName("parent") });
+
+  let type = aRow.getResultByName("type");
+
+  // Add annotations.
+  if (aRow.getResultByName("has_annos")) {
+    try {
+      bookmark.annos = PlacesUtils.getAnnotationsForItem(bookmark.id);
+    } catch (e) {
+      Cu.reportError("Unexpected error while reading annotations " + e);
+    }
+  }
+
+  switch (type) {
+    case Ci.nsINavBookmarksService.TYPE_BOOKMARK:
+      // TODO: What about shortcuts?
+      bookmark.type = PlacesUtils.TYPE_X_MOZ_PLACE;
+      // This will throw if we try to serialize an invalid url and the node will
+      // just be skipped.
+      bookmark.uri = NetUtil.newURI(aRow.getResultByName("url")).spec;
+      // Keywords are cached, so this should be decently fast.
+      let keyword = PlacesUtils.bookmarks.getKeywordForBookmark(bookmark.id);
+      if (keyword)
+        bookmark.keyword = keyword;
+      let charset = aRow.getResultByName("charset");
+      if (charset)
+        bookmark.charset = charset;
+      let tags = aRow.getResultByName("tags");
+      if (tags)
+        bookmark.tags = tags;
+      break;
+    case Ci.nsINavBookmarksService.TYPE_FOLDER:
+      bookmark.type = PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER;
+
+      // Mark root folders.
+      if (bookmark.id == PlacesUtils.placesRootId)
+        bookmark.root = "placesRoot";
+      else if (bookmark.id == PlacesUtils.bookmarksMenuFolderId)
+        bookmark.root = "bookmarksMenuFolder";
+      else if (bookmark.id == PlacesUtils.unfiledBookmarksFolderId)
+        bookmark.root = "unfiledBookmarksFolder";
+      else if (bookmark.id == PlacesUtils.toolbarFolderId)
+        bookmark.root = "toolbarFolder";
+      break;
+    case Ci.nsINavBookmarksService.TYPE_SEPARATOR:
+      bookmark.type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR;
+      break;
+    default:
+      Cu.reportError("Unexpected bookmark type");
+      break;
+  }
+  return bookmark;
 }
--- a/toolkit/components/places/tests/bookmarks/test_466303-json-remove-backups.js
+++ b/toolkit/components/places/tests/bookmarks/test_466303-json-remove-backups.js
@@ -1,66 +1,133 @@
 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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/. */
 
-const NUMBER_OF_BACKUPS = 1;
+// Since PlacesBackups.getbackupFiles() is a lazy getter, these tests must
+// run in the given order, to avoid making it out-of-sync.
+
+add_task(function check_max_backups_is_respected() {
+  // Get bookmarkBackups directory
+  let backupFolder = yield PlacesBackups.getBackupFolder();
+
+  // Create an html dummy backup in the past.
+  let htmlPath = OS.Path.join(backupFolder, "bookmarks-2008-01-01.html");
+  let htmlFile = yield OS.File.open(htmlPath, { truncate: true });
+  htmlFile.close();
+  do_check_true(yield OS.File.exists(htmlPath));
+
+  // Create a json dummy backup in the past.
+  let jsonPath = OS.Path.join(backupFolder, "bookmarks-2008-01-31.json");
+  let jsonFile = yield OS.File.open(jsonPath, { truncate: true });
+  jsonFile.close();
+  do_check_true(yield OS.File.exists(jsonPath));
+
+  // Export bookmarks to JSON.
+  // Allow 2 backups, the older one should be removed.
+  yield PlacesBackups.create(2);
+  let backupFilename = PlacesBackups.getFilenameForDate();
+  let re = new RegExp("^" + backupFilename.replace(/\.json/, "") + "(_[0-9]+){0,1}\.json$");
+
+  let count = 0;
+  let lastBackupPath = null;
+  let iterator = new OS.File.DirectoryIterator(backupFolder);
+  try {
+    yield iterator.forEach(aEntry => {
+      count++;
+      if (aEntry.name.match(re))
+        lastBackupPath = aEntry.path;
+    });
+  } finally {
+    iterator.close();
+  }
+
+  do_check_eq(count, 2);
+  do_check_neq(lastBackupPath, null);
+  do_check_false(yield OS.File.exists(htmlPath));
+  do_check_true(yield OS.File.exists(jsonPath));
+});
+
+add_task(function check_max_backups_greater_than_backups() {
+  // Get bookmarkBackups directory
+  let backupFolder = yield PlacesBackups.getBackupFolder();
+
+  // Export bookmarks to JSON.
+  // Allow 3 backups, none should be removed.
+  yield PlacesBackups.create(3);
+  let backupFilename = PlacesBackups.getFilenameForDate();
+  let re = new RegExp("^" + backupFilename.replace(/\.json/, "") + "(_[0-9]+){0,1}\.json$");
+
+  let count = 0;
+  let lastBackupPath = null;
+  let iterator = new OS.File.DirectoryIterator(backupFolder);
+  try {
+    yield iterator.forEach(aEntry => {
+      count++;
+      if (aEntry.name.match(re))
+        lastBackupPath = aEntry.path;
+    });
+  } finally {
+    iterator.close();
+  }
+  do_check_eq(count, 2);
+  do_check_neq(lastBackupPath, null);
+});
+
+add_task(function check_max_backups_null() {
+  // Get bookmarkBackups directory
+  let backupFolder = yield PlacesBackups.getBackupFolder();
+
+  // Export bookmarks to JSON.
+  // Allow infinite backups, none should be removed, a new one is not created
+  // since one for today already exists.
+  yield PlacesBackups.create(null);
+  let backupFilename = PlacesBackups.getFilenameForDate();
+  let re = new RegExp("^" + backupFilename.replace(/\.json/, "") + "(_[0-9]+){0,1}\.json$");
+
+  let count = 0;
+  let lastBackupPath = null;
+  let iterator = new OS.File.DirectoryIterator(backupFolder);
+  try {
+    yield iterator.forEach(aEntry => {
+      count++;
+      if (aEntry.name.match(re))
+        lastBackupPath = aEntry.path;
+    });
+  } finally {
+    iterator.close();
+  }
+  do_check_eq(count, 2);
+  do_check_neq(lastBackupPath, null);
+});
+
+add_task(function check_max_backups_undefined() {
+  // Get bookmarkBackups directory
+  let backupFolder = yield PlacesBackups.getBackupFolder();
+
+  // Export bookmarks to JSON.
+  // Allow infinite backups, none should be removed, a new one is not created
+  // since one for today already exists.
+  yield PlacesBackups.create();
+  let backupFilename = PlacesBackups.getFilenameForDate();
+  let re = new RegExp("^" + backupFilename.replace(/\.json/, "") + "(_[0-9]+){0,1}\.json$");
+
+  let count = 0;
+  let lastBackupPath = null;
+  let iterator = new OS.File.DirectoryIterator(backupFolder);
+  try {
+    yield iterator.forEach(aEntry => {
+      count++;
+      if (aEntry.name.match(re))
+        lastBackupPath = aEntry.path;
+    });
+  } finally {
+    iterator.close();
+  }
+  do_check_eq(count, 2);
+  do_check_neq(lastBackupPath, null);
+});
 
 function run_test() {
-  do_test_pending();
-
-  Task.spawn(function() {
-    // Get bookmarkBackups directory
-    let backupFolder = yield PlacesBackups.getBackupFolder();
-    let bookmarksBackupDir = new FileUtils.File(backupFolder);
-
-    // Create an html dummy backup in the past
-    var htmlBackupFile = bookmarksBackupDir.clone();
-    htmlBackupFile.append("bookmarks-2008-01-01.html");
-    if (htmlBackupFile.exists())
-      htmlBackupFile.remove(false);
-    do_check_false(htmlBackupFile.exists());
-    htmlBackupFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, 0600);
-    do_check_true(htmlBackupFile.exists());
-
-    // Create a json dummy backup in the past
-    var jsonBackupFile = bookmarksBackupDir.clone();
-    jsonBackupFile.append("bookmarks-2008-01-31.json");
-    if (jsonBackupFile.exists())
-      jsonBackupFile.remove(false);
-    do_check_false(jsonBackupFile.exists());
-    jsonBackupFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, 0600);
-    do_check_true(jsonBackupFile.exists());
-
-    // Export bookmarks to JSON.
-    var backupFilename = PlacesBackups.getFilenameForDate();
-    var rx = new RegExp("^" + backupFilename.replace(/\.json/, "") + "(_[0-9]+){0,1}\.json$");
-    var files = bookmarksBackupDir.directoryEntries;
-    var entry;
-    while (files.hasMoreElements()) {
-      entry = files.getNext().QueryInterface(Ci.nsIFile);
-      if (entry.leafName.match(rx))
-        entry.remove(false);
-    }
-
-    yield PlacesBackups.create(NUMBER_OF_BACKUPS);
-    files = bookmarksBackupDir.directoryEntries;
-    while (files.hasMoreElements()) {
-      entry = files.getNext().QueryInterface(Ci.nsIFile);
-      if (entry.leafName.match(rx))
-        lastBackupFile = entry;
-    }
-    do_check_true(lastBackupFile.exists());
-
-    // Check that last backup has been retained
-    do_check_false(htmlBackupFile.exists());
-    do_check_false(jsonBackupFile.exists());
-    do_check_true(lastBackupFile.exists());
-
-    // cleanup
-    lastBackupFile.remove(false);
-    do_check_false(lastBackupFile.exists());
-
-    do_test_finished();
-  });
+  run_next_test();
 }
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -2839,16 +2839,40 @@
   "PLACES_KEYWORDS_COUNT": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": "200",
     "n_buckets": 10,
     "extended_statistics_ok": true,
     "description": "PLACES: Number of keywords"
   },
+  "PLACES_BACKUPS_DAYSFROMLAST": {
+    "expires_in_version": "never",
+    "kind": "enumerated",
+    "n_values": 15,
+    "description": "PLACES: Days from last backup"
+  },
+  "PLACES_BACKUPS_BOOKMARKSTREE_MS": {
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "low": 50,
+    "high": 2000,
+    "n_buckets": 10,
+    "extended_statistics_ok": true,
+    "description": "PLACES: Time to build the bookmarks tree"
+  },
+  "PLACES_BACKUPS_TOJSON_MS": {
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "low": 50,
+    "high": 2000,
+    "n_buckets": 10,
+    "extended_statistics_ok": true,
+    "description": "PLACES: Time to convert and write the backup"
+  },
   "FENNEC_FAVICONS_COUNT": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": "2000",
     "n_buckets": 10,
     "cpp_guard": "ANDROID",
     "extended_statistics_ok": true,
     "description": "FENNEC: (Places) Number of favicons stored"