Bug 824433 - Bookmarks backup takes a long time to write out on shutdown. r=mano
authorMarco Bonardo <mbonardo@mozilla.com>
Tue, 04 Feb 2014 14:43:20 +0100
changeset 167567 af5cdb31f131d9d3fcda9c4d95f4adb50346c2d7
parent 167566 3a10c46057955b7000a0ad917094d7fb668bb3db
child 167568 9fb9effa74a6ddf68d15c02dca1ca7c21476a150
push id26174
push userkwierso@gmail.com
push dateSat, 08 Feb 2014 00:55:48 +0000
treeherdermozilla-central@2c873eff7dc2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmano
bugs824433
milestone30.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 824433 - Bookmarks backup takes a long time to write out on shutdown. r=mano
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/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
@@ -11,13 +11,12 @@ support-files =
 [test_421483.js]
 [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,70 @@ 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.");
+      }
+
+      // Write to the temp folder first, to avoid leaving back partial files.
+      let tmpPath = OS.Path.join(OS.Constants.Path.tmpDir,
+                                 OS.Path.basename(aFilePath) + ".tmp");
+      yield OS.File.writeAtomic(aFilePath, jsonString, { tmpPath: tmpPath });
+      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 +394,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 +455,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 +536,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 +560,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 +597,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 +666,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 +746,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 +776,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,96 +40,104 @@ 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) {
         let matches = aEntry.name.match(this._filenamesRegex);
         if (matches) {
           // Remove bogus backups in future dates.
@@ -133,23 +146,23 @@ this.PlacesBackups = {
             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 +202,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,17 +304,17 @@ 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();
@@ -309,17 +326,17 @@ this.PlacesBackups = {
           // the total backups after this operation does not exceed the
           // number specified in the pref.
           if (!mostRecentBackupFile ||
               !this._isFilenameWithSameDate(OS.Path.basename(mostRecentBackupFile),
                                             newBackupFilename))
             numberOfBackupsToDelete++;
 
           while (numberOfBackupsToDelete--) {
-            this.entries.pop();
+            this._entries.pop();
             if (!this._backupFiles) {
               yield this.getBackupFiles();
             }
             let oldestBackup = this._backupFiles.pop();
             yield OS.File.remove(oldestBackup);
           }
         }
 
@@ -336,35 +353,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 +409,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/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"