Bug 1089695 - Async sanitize.js;r=mak
authorDavid Rajchenbach-Teller <dteller@mozilla.com>
Mon, 27 Jul 2015 19:18:19 +0200
changeset 265165 9a6d45264eb260789068d0e6a19bcffff1d1d16b
parent 265164 b8f3cba5419fd2f2a247b0e136e6d2236a247bc5
child 265166 bbefa0967bad3e180a48d81a29d1e95e2534d320
push id29457
push userkwierso@gmail.com
push dateWed, 30 Sep 2015 20:47:20 +0000
treeherdermozilla-central@7287334e8cdc [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak
bugs1089695
milestone44.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 1089695 - Async sanitize.js;r=mak
browser/base/content/browser.js
browser/base/content/sanitize.js
browser/base/content/sanitizeDialog.js
browser/base/content/test/general/browser_sanitize-passwordDisabledHosts.js
browser/base/content/test/general/browser_sanitize-sitepermissions.js
browser/base/content/test/general/browser_sanitizeDialog.js
browser/base/jar.mn
browser/components/nsBrowserGlue.js
browser/components/places/tests/unit/test_clearHistory_shutdown.js
browser/modules/Sanitizer.jsm
browser/modules/moz.build
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1283,19 +1283,16 @@ var gBrowserInit = {
 
     UpdateUrlbarSearchSplitterState();
 
     if (!(isBlankPageURL(uriToLoad) || uriToLoad == "about:privatebrowsing") ||
         !focusAndSelectUrlBar()) {
       gBrowser.selectedBrowser.focus();
     }
 
-    // Set up Sanitize Item
-    this._initializeSanitizer();
-
     // Enable/Disable auto-hide tabbar
     gBrowser.tabContainer.updateVisibility();
 
     BookmarkingUI.init();
 
     gPrefService.addObserver(gHomeButton.prefDomain, gHomeButton, false);
 
     var homeButton = document.getElementById("home-button");
@@ -1670,19 +1667,16 @@ var gBrowserInit = {
   },
 
   nonBrowserWindowDelayedStartup: function() {
     this._delayedStartupTimeoutId = null;
 
     // initialise the offline listener
     BrowserOffline.init();
 
-    // Set up Sanitize Item
-    this._initializeSanitizer();
-
     // initialize the private browsing UI
     gPrivateBrowsingUI.init();
 
 #ifdef MOZ_SERVICES_SYNC
     // initialize the sync UI
     gSyncUI.init();
 #endif
 
@@ -1697,62 +1691,16 @@ var gBrowserInit = {
     if (this._delayedStartupTimeoutId) {
       clearTimeout(this._delayedStartupTimeoutId);
       return;
     }
 
     BrowserOffline.uninit();
   },
 #endif
-
-  _initializeSanitizer: function() {
-    const kDidSanitizeDomain = "privacy.sanitize.didShutdownSanitize";
-    if (gPrefService.prefHasUserValue(kDidSanitizeDomain)) {
-      gPrefService.clearUserPref(kDidSanitizeDomain);
-      // We need to persist this preference change, since we want to
-      // check it at next app start even if the browser exits abruptly
-      gPrefService.savePrefFile(null);
-    }
-
-    /**
-     * Migrate Firefox 3.0 privacy.item prefs under one of these conditions:
-     *
-     * a) User has customized any privacy.item prefs
-     * b) privacy.sanitize.sanitizeOnShutdown is set
-     */
-    if (!gPrefService.getBoolPref("privacy.sanitize.migrateFx3Prefs")) {
-      let itemBranch = gPrefService.getBranch("privacy.item.");
-      let itemArray = itemBranch.getChildList("");
-
-      // See if any privacy.item prefs are set
-      let doMigrate = itemArray.some(function (name) itemBranch.prefHasUserValue(name));
-      // Or if sanitizeOnShutdown is set
-      if (!doMigrate)
-        doMigrate = gPrefService.getBoolPref("privacy.sanitize.sanitizeOnShutdown");
-
-      if (doMigrate) {
-        let cpdBranch = gPrefService.getBranch("privacy.cpd.");
-        let clearOnShutdownBranch = gPrefService.getBranch("privacy.clearOnShutdown.");
-        for (let name of itemArray) {
-          try {
-            // don't migrate password or offlineApps clearing in the CRH dialog since
-            // there's no UI for those anymore. They default to false. bug 497656
-            if (name != "passwords" && name != "offlineApps")
-              cpdBranch.setBoolPref(name, itemBranch.getBoolPref(name));
-            clearOnShutdownBranch.setBoolPref(name, itemBranch.getBoolPref(name));
-          }
-          catch(e) {
-            Cu.reportError("Exception thrown during privacy pref migration: " + e);
-          }
-        }
-      }
-
-      gPrefService.setBoolPref("privacy.sanitize.migrateFx3Prefs", true);
-    }
-  },
 }
 
 
 /* Legacy global init functions */
 var BrowserStartup        = gBrowserInit.onLoad.bind(gBrowserInit);
 var BrowserShutdown       = gBrowserInit.onUnload.bind(gBrowserInit);
 #ifdef XP_MACOSX
 var nonBrowserWindowStartup        = gBrowserInit.nonBrowserWindowStartup.bind(gBrowserInit);
--- a/browser/base/content/sanitize.js
+++ b/browser/base/content/sanitize.js
@@ -1,39 +1,62 @@
-# -*- indent-tabs-mode: nil; js-indent-level: 4 -*-
-# 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/.
+// -*- indent-tabs-mode: nil; js-indent-level: 4 -*-
+/* 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/. */
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+                                  "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
                                   "resource://gre/modules/FormHistory.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
                                   "resource://gre/modules/Downloads.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon",
                                   "resource:///modules/DownloadsCommon.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
                                   "resource://gre/modules/TelemetryStopwatch.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "console",
+                                  "resource://gre/modules/devtools/Console.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+                                  "resource://gre/modules/Preferences.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
+                                  "resource://gre/modules/Timer.jsm");
 
-function Sanitizer() {}
+/**
+ * A number of iterations after which to yield time back
+ * to the system.
+ */
+const YIELD_PERIOD = 10;
+
+function Sanitizer() {
+}
 Sanitizer.prototype = {
   // warning to the caller: this one may raise an exception (e.g. bug #265028)
   clearItem: function (aItemName)
   {
     if (this.items[aItemName].canClear)
       this.items[aItemName].clear();
   },
 
+  promiseCanClearItem: function (aItemName, aArg) {
+    return new Promise(resolve => {
+      return this.canClearItem(aItemName,
+                              (_, canClear) => resolve(canClear),
+                              aArg)
+    });
+  },
+
   canClearItem: function (aItemName, aCallback, aArg)
   {
     let canClear = this.items[aItemName].canClear;
     if (typeof canClear == "function") {
       canClear(aCallback, aArg);
       return false;
     }
 
@@ -52,311 +75,311 @@ Sanitizer.prototype = {
    * Deletes privacy sensitive data in a batch, according to user preferences.
    * Returns a promise which is resolved if no errors occurred.  If an error
    * occurs, a message is reported to the console and all other items are still
    * cleared before the promise is finally rejected.
    *
    * If the consumer specifies the (optional) array parameter, only those
    * items get cleared (irrespective of the preference settings)
    */
-  sanitize: function (aItemsToClear)
-  {
-    var deferred = Promise.defer();
-    var seenError = false;
+  sanitize: Task.async(function*(aItemsToClear = null) {
+    let progress = {};
+    let promise = this._sanitize(aItemsToClear, progress);
+
+    //
+    // Depending on preferences, the sanitizer may perform asynchronous
+    // work before it starts cleaning up the Places database (e.g. closing
+    // windows). We need to make sure that the connection to that database
+    // hasn't been closed by the time we use it.
+    //
+    let shutdownClient = Cc["@mozilla.org/browser/nav-history-service;1"]
+       .getService(Ci.nsPIPlacesDatabase)
+       .shutdownClient
+       .jsclient;
+
+    shutdownClient.addBlocker("sanitize.js: Sanitize",
+      promise,
+      {
+        fetchState: () => {
+          return { progress };
+        }
+      }
+    );
+    try {
+      yield promise;
+    } finally {
+      Services.obs.notifyObservers(null, "sanitizer-sanitization-complete", "");
+    }
+  }),
+
+  _sanitize: Task.async(function*(aItemsToClear, progress = {}) {
+    let seenError = false;
+    let itemsToClear;
     if (Array.isArray(aItemsToClear)) {
-      var itemsToClear = [...aItemsToClear];
+      // Shallow copy the array, as we are going to modify
+      // it in place later.
+      itemsToClear = [...aItemsToClear];
     } else {
       let branch = Services.prefs.getBranch(this.prefDomain);
       itemsToClear = Object.keys(this.items).filter(itemName => {
         try {
           return branch.getBoolPref(itemName);
         } catch (ex) {
           return false;
         }
       });
     }
 
+    // Store the list of items to clear, in case we are killed before we
+    // get a chance to complete.
+    Preferences.set(Sanitizer.PREF_SANITIZE_IN_PROGRESS, JSON.stringify(itemsToClear));
+
+    // Store the list of items to clear, for debugging/forensics purposes
+    for (let k of itemsToClear) {
+      progress[k] = "ready";
+    }
+
     // Ensure open windows get cleared first, if they're in our list, so that they don't stick
     // around in the recently closed windows list, and so we can cancel the whole thing
     // if the user selects to keep a window open from a beforeunload prompt.
     let openWindowsIndex = itemsToClear.indexOf("openWindows");
     if (openWindowsIndex != -1) {
       itemsToClear.splice(openWindowsIndex, 1);
-      let item = this.items.openWindows;
+      yield this.items.openWindows.clear();
+      progress.openWindows = "cleared";
+    }
 
-      let ok = item.clear(() => {
-        try {
-          let clearedPromise = this.sanitize(itemsToClear);
-          clearedPromise.then(deferred.resolve, deferred.reject);
-        } catch(e) {
-          let error = "Sanitizer threw after closing windows: " + e;
-          Cu.reportError(error);
-          deferred.reject(error);
-        }
-      });
-      // When cancelled, reject immediately
-      if (!ok) {
-        deferred.reject("Sanitizer canceled closing windows");
-      }
-
-      return deferred.promise;
+    // Cache the range of times to clear
+    let range = null;
+    // If we ignore timespan, clear everything,
+    // otherwise, pick a range.
+    if (!this.ignoreTimespan) {
+      range = this.range || Sanitizer.getClearRange();
     }
 
-    let cookiesIndex = itemsToClear.indexOf("cookies");
-    if (cookiesIndex != -1) {
-      itemsToClear.splice(cookiesIndex, 1);
-      let item = this.items.cookies;
-      item.range = this.range;
-      let ok = item.clear(() => {
-        try {
-          if (!itemsToClear.length) {
-            // we're done
-            deferred.resolve();
-            return;
-          }
-          let clearedPromise = this.sanitize(itemsToClear);
-          clearedPromise.then(deferred.resolve, deferred.reject);
-        } catch(e) {
-          let error = "Sanitizer threw after clearing cookies: " + e;
-          Cu.reportError(error);
-          deferred.reject(error);
-        }
-      });
-      // When cancelled, reject immediately
-      if (!ok) {
-        deferred.reject("Sanitizer canceled clearing cookies");
-      }
-
-      return deferred.promise;
-    }
-
-    TelemetryStopwatch.start("FX_SANITIZE_TOTAL");
-
-    // Cache the range of times to clear
-    if (this.ignoreTimespan)
-      var range = null;  // If we ignore timespan, clear everything
-    else
-      range = this.range || Sanitizer.getClearRange();
-
-    let itemCount = Object.keys(itemsToClear).length;
-    let onItemComplete = function() {
-      if (!--itemCount) {
-        TelemetryStopwatch.finish("FX_SANITIZE_TOTAL");
-        seenError ? deferred.reject() : deferred.resolve();
-      }
-    };
     for (let itemName of itemsToClear) {
       let item = this.items[itemName];
+      if (!("clear" in item)) {
+        progress[itemName] = "`clear` not in item";
+        continue;
+      }
       item.range = range;
-      if ("clear" in item) {
-        let clearCallback = (itemName, aCanClear) => {
-          // Some of these clear() may raise exceptions (see bug #265028)
-          // to sanitize as much as possible, we catch and store them,
-          // rather than fail fast.
-          // Callers should check returned errors and give user feedback
-          // about items that could not be sanitized
-          let item = this.items[itemName];
-          try {
-            if (aCanClear)
-              item.clear();
-          } catch(er) {
-            seenError = true;
-            Components.utils.reportError("Error sanitizing " + itemName +
-                                         ": " + er + "\n");
-          }
-          onItemComplete();
-        };
-        this.canClearItem(itemName, clearCallback);
-      } else {
-        onItemComplete();
+      let canClear = yield this.promiseCanClearItem(itemName);
+      if (!canClear) {
+        progress[itemName] = "cannot clear item";
+        continue;
+      }
+      // Some of these clear() may raise exceptions (see bug #265028)
+      // to sanitize as much as possible, we catch and store them,
+      // rather than fail fast.
+      // Callers should check returned errors and give user feedback
+      // about items that could not be sanitized
+      let refObj = {};
+      try {
+        TelemetryStopwatch.start("FX_SANITIZE_TOTAL", refObj);
+        yield item.clear();
+        progress[itemName] = "cleared";
+      } catch(er) {
+        progress[itemName] = "failed";
+        seenError = true;
+        console.error("Error sanitizing " + itemName, er);
+      } finally {
+        TelemetryStopwatch.finish("FX_SANITIZE_TOTAL", refObj);
       }
     }
 
-    return deferred.promise;
-  },
+    // Sanitization is complete.
+    Preferences.reset(Sanitizer.PREF_SANITIZE_IN_PROGRESS);
+    progress = {};
+    if (seenError) {
+      throw new Error("Error sanitizing");
+    }
+  }),
 
   // Time span only makes sense in certain cases.  Consumers who want
   // to only clear some private data can opt in by setting this to false,
   // and can optionally specify a specific range.  If timespan is not ignored,
   // and range is not set, sanitize() will use the value of the timespan
   // pref to determine a range
   ignoreTimespan : true,
   range : null,
 
   items: {
     cache: {
       clear: function ()
       {
-        TelemetryStopwatch.start("FX_SANITIZE_CACHE");
+        let refObj = {};
+        TelemetryStopwatch.start("FX_SANITIZE_CACHE", refObj);
 
         var cache = Cc["@mozilla.org/netwerk/cache-storage-service;1"].
                     getService(Ci.nsICacheStorageService);
         try {
           // Cache doesn't consult timespan, nor does it have the
           // facility for timespan-based eviction.  Wipe it.
           cache.clear();
         } catch(er) {}
 
         var imageCache = Cc["@mozilla.org/image/tools;1"].
                          getService(Ci.imgITools).getImgCacheForDocument(null);
         try {
           imageCache.clearCache(false); // true=chrome, false=content
         } catch(er) {}
 
-        TelemetryStopwatch.finish("FX_SANITIZE_CACHE");
+        TelemetryStopwatch.finish("FX_SANITIZE_CACHE", refObj);
       },
 
       get canClear()
       {
         return true;
       }
     },
 
     cookies: {
-      clear: function (aCallback)
+      clear: Task.async(function* ()
       {
-        TelemetryStopwatch.start("FX_SANITIZE_COOKIES");
-        TelemetryStopwatch.start("FX_SANITIZE_COOKIES_2");
+        let yieldCounter = 0;
+        let refObj = {};
+        TelemetryStopwatch.start("FX_SANITIZE_COOKIES", refObj);
+        TelemetryStopwatch.start("FX_SANITIZE_COOKIES_2", refObj);
 
         var cookieMgr = Components.classes["@mozilla.org/cookiemanager;1"]
                                   .getService(Ci.nsICookieManager);
         if (this.range) {
           // Iterate through the cookies and delete any created after our cutoff.
           var cookiesEnum = cookieMgr.enumerator;
           while (cookiesEnum.hasMoreElements()) {
             var cookie = cookiesEnum.getNext().QueryInterface(Ci.nsICookie2);
 
-            if (cookie.creationTime > this.range[0])
+            if (cookie.creationTime > this.range[0]) {
               // This cookie was created after our cutoff, clear it
               cookieMgr.remove(cookie.host, cookie.name, cookie.path, false);
+
+              if (++yieldCounter % YIELD_PERIOD == 0) {
+                yield new Promise(resolve => setTimeout(resolve, 0)); // Don't block the main thread too long
+              }
+            }
           }
         }
         else {
           // Remove everything
           cookieMgr.removeAll();
+          yield new Promise(resolve => setTimeout(resolve, 0)); // Don't block the main thread too long
         }
-
-        TelemetryStopwatch.finish("FX_SANITIZE_COOKIES_2");
+        TelemetryStopwatch.finish("FX_SANITIZE_COOKIES_2", refObj);
 
         // Clear deviceIds. Done asynchronously (returns before complete).
         let mediaMgr = Components.classes["@mozilla.org/mediaManagerService;1"]
                                  .getService(Ci.nsIMediaManagerService);
         mediaMgr.sanitizeDeviceIds(this.range && this.range[0]);
 
         // Clear plugin data.
-        TelemetryStopwatch.start("FX_SANITIZE_PLUGINS");
-        this.clearPluginCookies().then(
-          function() {
-            TelemetryStopwatch.finish("FX_SANITIZE_PLUGINS");
-            TelemetryStopwatch.finish("FX_SANITIZE_COOKIES");
-            aCallback();
-          });
-        return true;
-      },
+        TelemetryStopwatch.start("FX_SANITIZE_PLUGINS", refObj);
+        yield this.promiseClearPluginCookies();
+        TelemetryStopwatch.finish("FX_SANITIZE_PLUGINS", refObj);
+        TelemetryStopwatch.finish("FX_SANITIZE_COOKIES", refObj);
+      }),
 
-      clearPluginCookies: function() {
+      promiseClearPluginCookies: Task.async(function*() {
         const phInterface = Ci.nsIPluginHost;
         const FLAG_CLEAR_ALL = phInterface.FLAG_CLEAR_ALL;
         let ph = Cc["@mozilla.org/plugin/host;1"].getService(phInterface);
 
         // Determine age range in seconds. (-1 means clear all.) We don't know
         // that this.range[1] is actually now, so we compute age range based
         // on the lower bound. If this.range results in a negative age, do
         // nothing.
         let age = this.range ? (Date.now() / 1000 - this.range[0] / 1000000) : -1;
         if (!this.range || age >= 0) {
           let tags = ph.getPluginTags();
-          function iterate(tag) {
-            let promise = new Promise(resolve => {
-              try {
-                let onClear = function(rv) {
-                  // If the plugin doesn't support clearing by age, clear everything.
-                  if (rv == Components.results. NS_ERROR_PLUGIN_TIME_RANGE_NOT_SUPPORTED) {
-                    ph.clearSiteData(tag, null, FLAG_CLEAR_ALL, -1, function() {
-                      resolve();
-                    });
-                  } else {
-                    resolve();
-                  }
-                };
-                ph.clearSiteData(tag, null, FLAG_CLEAR_ALL, age, onClear);
-              } catch (ex) {
-                resolve();
+          for (let tag of tags) {
+            try {
+              let rv = yield new Promise(resolve =>
+                ph.clearSiteData(tag, null, FLAG_CLEAR_ALL, age, resolve)
+              );
+              // If the plugin doesn't support clearing by age, clear everything.
+              if (rv == Components.results.NS_ERROR_PLUGIN_TIME_RANGE_NOT_SUPPORTED) {
+                yield new Promise(resolve =>
+                  ph.clearSiteData(tag, null, FLAG_CLEAR_ALL, -1, resolve)
+                );
               }
-            });
-            return promise;
+            } catch (ex) {
+              // Ignore errors from plug-ins
+            }
           }
-          let promises = [];
-          for (let tag of tags) {
-            promises.push(iterate(tag));
-          }
-          return Promise.all(promises);
         }
-      },
+      }),
 
       get canClear()
       {
         return true;
       }
     },
 
     offlineApps: {
       clear: function ()
       {
-        TelemetryStopwatch.start("FX_SANITIZE_OFFLINEAPPS");
+        let refObj = {};
+        TelemetryStopwatch.start("FX_SANITIZE_OFFLINEAPPS", refObj);
         Components.utils.import("resource:///modules/offlineAppCache.jsm");
         OfflineAppCacheHelper.clear();
-        TelemetryStopwatch.finish("FX_SANITIZE_OFFLINEAPPS");
+        TelemetryStopwatch.finish("FX_SANITIZE_OFFLINEAPPS", refObj);
       },
 
       get canClear()
       {
         return true;
       }
     },
 
     history: {
-      clear: function ()
+      clear: Task.async(function* ()
       {
-        TelemetryStopwatch.start("FX_SANITIZE_HISTORY");
-
-        if (this.range)
-          PlacesUtils.history.removeVisitsByTimeframe(this.range[0], this.range[1]);
-        else
-          PlacesUtils.history.removeAllPages();
-
+        let refObj = {};
+        TelemetryStopwatch.start("FX_SANITIZE_HISTORY", refObj);
         try {
-          var os = Components.classes["@mozilla.org/observer-service;1"]
-                             .getService(Components.interfaces.nsIObserverService);
-          let clearStartingTime = this.range ? String(this.range[0]) : "";
-          os.notifyObservers(null, "browser:purge-session-history", clearStartingTime);
-        }
-        catch (e) { }
+          if (this.range) {
+            yield PlacesUtils.history.removeVisitsByFilter({
+              beginDate: new Date(this.range[0] / 1000),
+              endDate: new Date(this.range[1] / 1000)
+            });
+          } else {
+            // Remove everything.
+            yield PlacesUtils.history.clear();
+          }
 
-        try {
-          var predictor = Components.classes["@mozilla.org/network/predictor;1"]
-                                    .getService(Components.interfaces.nsINetworkPredictor);
-          predictor.reset();
-        } catch (e) { }
+          try {
+            let clearStartingTime = this.range ? String(this.range[0]) : "";
+            Services.obs.notifyObservers(null, "browser:purge-session-history", clearStartingTime);
+          } catch (e) { }
 
-        TelemetryStopwatch.finish("FX_SANITIZE_HISTORY");
-      },
+          try {
+            let predictor = Components.classes["@mozilla.org/network/predictor;1"]
+                                      .getService(Components.interfaces.nsINetworkPredictor);
+            predictor.reset();
+          } catch (e) {
+            console.error("Error while resetting the predictor", e);
+          }
+        } finally {
+          TelemetryStopwatch.finish("FX_SANITIZE_HISTORY", refObj);
+        }
+      }),
 
       get canClear()
       {
         // bug 347231: Always allow clearing history due to dependencies on
         // the browser:purge-session-history notification. (like error console)
         return true;
       }
     },
 
     formdata: {
       clear: function ()
       {
-        TelemetryStopwatch.start("FX_SANITIZE_FORMDATA");
+        let refObj = {};
+        TelemetryStopwatch.start("FX_SANITIZE_FORMDATA", refObj);
 
         // Clear undo history of all searchBars
         var windowManager = Components.classes['@mozilla.org/appshell/window-mediator;1']
                                       .getService(Components.interfaces.nsIWindowMediator);
         var windows = windowManager.getEnumerator("navigator:browser");
         while (windows.hasMoreElements()) {
           let currentWindow = windows.getNext();
           let currentDocument = currentWindow.document;
@@ -373,17 +396,17 @@ Sanitizer.prototype = {
         }
 
         let change = { op: "remove" };
         if (this.range) {
           [ change.firstUsedStart, change.firstUsedEnd ] = this.range;
         }
         FormHistory.update(change);
 
-        TelemetryStopwatch.finish("FX_SANITIZE_FORMDATA");
+        TelemetryStopwatch.finish("FX_SANITIZE_FORMDATA", refObj);
       },
 
       canClear : function(aCallback, aArg)
       {
         var windowManager = Components.classes['@mozilla.org/appshell/window-mediator;1']
                                       .getService(Components.interfaces.nsIWindowMediator);
         var windows = windowManager.getEnumerator("navigator:browser");
         while (windows.hasMoreElements()) {
@@ -420,72 +443,75 @@ Sanitizer.prototype = {
         FormHistory.count({}, countDone);
         return false;
       }
     },
 
     downloads: {
       clear: function ()
       {
-        TelemetryStopwatch.start("FX_SANITIZE_DOWNLOADS");
+        let refObj = {};
+        TelemetryStopwatch.start("FX_SANITIZE_DOWNLOADS", refObj);
         Task.spawn(function () {
           let filterByTime = null;
           if (this.range) {
             // Convert microseconds back to milliseconds for date comparisons.
             let rangeBeginMs = this.range[0] / 1000;
             let rangeEndMs = this.range[1] / 1000;
             filterByTime = download => download.startTime >= rangeBeginMs &&
                                        download.startTime <= rangeEndMs;
           }
 
           // Clear all completed/cancelled downloads
           let list = yield Downloads.getList(Downloads.ALL);
           list.removeFinished(filterByTime);
-          TelemetryStopwatch.finish("FX_SANITIZE_DOWNLOADS");
+          TelemetryStopwatch.finish("FX_SANITIZE_DOWNLOADS", refObj);
         }.bind(this)).then(null, error => {
-          TelemetryStopwatch.finish("FX_SANITIZE_DOWNLOADS");
+          TelemetryStopwatch.finish("FX_SANITIZE_DOWNLOADS", refObj);
           Components.utils.reportError(error);
         });
       },
 
       canClear : function(aCallback, aArg)
       {
         aCallback("downloads", true, aArg);
         return false;
       }
     },
 
     sessions: {
       clear: function ()
       {
-        TelemetryStopwatch.start("FX_SANITIZE_SESSIONS");
+        let refObj = {};
+        TelemetryStopwatch.start("FX_SANITIZE_SESSIONS", refObj);
 
         // clear all auth tokens
         var sdr = Components.classes["@mozilla.org/security/sdr;1"]
                             .getService(Components.interfaces.nsISecretDecoderRing);
         sdr.logoutAndTeardown();
 
         // clear FTP and plain HTTP auth sessions
         var os = Components.classes["@mozilla.org/observer-service;1"]
                            .getService(Components.interfaces.nsIObserverService);
         os.notifyObservers(null, "net:clear-active-logins", null);
 
-        TelemetryStopwatch.finish("FX_SANITIZE_SESSIONS");
+        TelemetryStopwatch.finish("FX_SANITIZE_SESSIONS", refObj);
       },
 
       get canClear()
       {
         return true;
       }
     },
 
     siteSettings: {
       clear: function ()
       {
-        TelemetryStopwatch.start("FX_SANITIZE_SITESETTINGS");
+        let refObj = {};
+        TelemetryStopwatch.start("FX_SANITIZE_SITESETTINGS", refObj);
 
         // Clear site-specific permissions like "Allow this site to open popups"
         // we ignore the "end" range and hope it is now() - none of the
         // interfaces used here support a true range anyway.
         let startDateMS = this.range == null ? null : this.range[0] / 1000;
         var pm = Components.classes["@mozilla.org/permissionmanager;1"]
                            .getService(Components.interfaces.nsIPermissionManager);
         if (startDateMS == null) {
@@ -525,17 +551,17 @@ Sanitizer.prototype = {
         try {
           var push = Cc["@mozilla.org/push/NotificationService;1"]
                       .getService(Ci.nsIPushNotificationService);
           push.clearAll();
         } catch (e) {
           dump("Web Push may not be available.\n");
         }
 
-        TelemetryStopwatch.finish("FX_SANITIZE_SITESETTINGS");
+        TelemetryStopwatch.finish("FX_SANITIZE_SITESETTINGS", refObj);
       },
 
       get canClear()
       {
         return true;
       }
     },
     openWindows: {
@@ -560,122 +586,138 @@ Sanitizer.prototype = {
         }
         return true;
       },
       _resetAllWindowClosures: function(aWindowList) {
         for (let win of aWindowList) {
           win.getInterface(Ci.nsIDocShell).contentViewer.resetCloseWindow();
         }
       },
-      clear: function(aCallback)
-      {
+      clear: Task.async(function*() {
         // NB: this closes all *browser* windows, not other windows like the library, about window,
         // browser console, etc.
 
-        if (!aCallback) {
-          throw "Sanitizer's openWindows clear() requires a callback.";
-        }
-
         // Keep track of the time in case we get stuck in la-la-land because of onbeforeunload
         // dialogs
         let existingWindow = Services.appShell.hiddenDOMWindow;
         let startDate = existingWindow.performance.now();
 
         // First check if all these windows are OK with being closed:
         let windowEnumerator = Services.wm.getEnumerator("navigator:browser");
         let windowList = [];
         while (windowEnumerator.hasMoreElements()) {
           let someWin = windowEnumerator.getNext();
           windowList.push(someWin);
           // If someone says "no" to a beforeunload prompt, we abort here:
           if (!this._canCloseWindow(someWin)) {
             this._resetAllWindowClosures(windowList);
-            return false;
+            throw new Error("Sanitize could not close windows: cancelled by user");
           }
 
           // ...however, beforeunload prompts spin the event loop, and so the code here won't get
           // hit until the prompt has been dismissed. If more than 1 minute has elapsed since we
           // started prompting, stop, because the user might not even remember initiating the
           // 'forget', and the timespans will be all wrong by now anyway:
           if (existingWindow.performance.now() > (startDate + 60 * 1000)) {
             this._resetAllWindowClosures(windowList);
-            return false;
+            throw new Error("Sanitize could not close windows: timeout");
           }
         }
 
         // If/once we get here, we should actually be able to close all windows.
 
-        TelemetryStopwatch.start("FX_SANITIZE_OPENWINDOWS");
+        let refObj = {};
+        TelemetryStopwatch.start("FX_SANITIZE_OPENWINDOWS", refObj);
 
         // First create a new window. We do this first so that on non-mac, we don't
         // accidentally close the app by closing all the windows.
         let handler = Cc["@mozilla.org/browser/clh;1"].getService(Ci.nsIBrowserHandler);
         let defaultArgs = handler.defaultArgs;
         let features = "chrome,all,dialog=no," + this.privateStateForNewWindow;
         let newWindow = existingWindow.openDialog("chrome://browser/content/", "_blank",
                                                   features, defaultArgs);
 
-        // Window creation and destruction is asynchronous. We need to wait
-        // until all existing windows are fully closed, and the new window is
-        // fully open, before continuing. Otherwise the rest of the sanitizer
-        // could run too early (and miss new cookies being set when a page
-        // closes) and/or run too late (and not have a fully-formed window yet
-        // in existence). See bug 1088137.
-        let newWindowOpened = false;
-        function onWindowOpened(subject, topic, data) {
-          if (subject != newWindow)
-            return;
-
-          Services.obs.removeObserver(onWindowOpened, "browser-delayed-startup-finished");
-          newWindowOpened = true;
-          // If we're the last thing to happen, invoke callback.
-          if (numWindowsClosing == 0) {
-            TelemetryStopwatch.finish("FX_SANITIZE_OPENWINDOWS");
-            aCallback();
+        if (AppConstants.platform == "macosx") {
+          let onFullScreen = function(e) {
+            newWindow.removeEventListener("fullscreen", onFullScreen);
+            let docEl = newWindow.document.documentElement;
+            let sizemode = docEl.getAttribute("sizemode");
+            if (!newWindow.fullScreen && sizemode == "fullscreen") {
+              docEl.setAttribute("sizemode", "normal");
+              e.preventDefault();
+              e.stopPropagation();
+              return false;
+            }
           }
+          newWindow.addEventListener("fullscreen", onFullScreen);
         }
 
-        let numWindowsClosing = windowList.length;
-        function onWindowClosed() {
-          numWindowsClosing--;
-          if (numWindowsClosing == 0) {
-            Services.obs.removeObserver(onWindowClosed, "xul-window-destroyed");
+        let promiseReady = new Promise(resolve => {
+          // Window creation and destruction is asynchronous. We need to wait
+          // until all existing windows are fully closed, and the new window is
+          // fully open, before continuing. Otherwise the rest of the sanitizer
+          // could run too early (and miss new cookies being set when a page
+          // closes) and/or run too late (and not have a fully-formed window yet
+          // in existence). See bug 1088137.
+          let newWindowOpened = false;
+          function onWindowOpened(subject, topic, data) {
+            if (subject != newWindow)
+              return;
+
+            Services.obs.removeObserver(onWindowOpened, "browser-delayed-startup-finished");
+            if (AppConstants.platform == "macosx") {
+              newWindow.removeEventListener("fullscreen", onFullScreen);
+            }
+            newWindowOpened = true;
             // If we're the last thing to happen, invoke callback.
-            if (newWindowOpened) {
-              TelemetryStopwatch.finish("FX_SANITIZE_OPENWINDOWS");
-              aCallback();
+            if (numWindowsClosing == 0) {
+              TelemetryStopwatch.finish("FX_SANITIZE_OPENWINDOWS", refObj);
+              resolve();
             }
           }
-        }
 
+          let numWindowsClosing = windowList.length;
+          function onWindowClosed() {
+            numWindowsClosing--;
+            if (numWindowsClosing == 0) {
+              Services.obs.removeObserver(onWindowClosed, "xul-window-destroyed");
+              // If we're the last thing to happen, invoke callback.
+              if (newWindowOpened) {
+                TelemetryStopwatch.finish("FX_SANITIZE_OPENWINDOWS", refObj);
+                resolve();
+              }
+            }
+          }
+        });
         Services.obs.addObserver(onWindowOpened, "browser-delayed-startup-finished", false);
         Services.obs.addObserver(onWindowClosed, "xul-window-destroyed", false);
 
         // Start the process of closing windows
         while (windowList.length) {
           windowList.pop().close();
         }
         newWindow.focus();
-        return true;
-      },
+        yield promiseReady;
+      }),
 
       get canClear()
       {
         return true;
       }
     },
   }
 };
 
 
 
 // "Static" members
-Sanitizer.prefDomain          = "privacy.sanitize.";
-Sanitizer.prefShutdown        = "sanitizeOnShutdown";
-Sanitizer.prefDidShutdown     = "didShutdownSanitize";
+Sanitizer.PREF_DOMAIN = "privacy.sanitize.";
+Sanitizer.PREF_SANITIZE_ON_SHUTDOWN = "privacy.sanitize.sanitizeOnShutdown";
+Sanitizer.PREF_SANITIZE_IN_PROGRESS = "privacy.sanitize.sanitizeInProgress";
+Sanitizer.PREF_SANITIZE_DID_SHUTDOWN = "privacy.sanitize.didShutdownSanitize";
 
 // Time span constants corresponding to values of the privacy.sanitize.timeSpan
 // pref.  Used to determine how much history to clear, for various items
 Sanitizer.TIMESPAN_EVERYTHING = 0;
 Sanitizer.TIMESPAN_HOUR       = 1;
 Sanitizer.TIMESPAN_2HOURS     = 2;
 Sanitizer.TIMESPAN_4HOURS     = 3;
 Sanitizer.TIMESPAN_TODAY      = 4;
@@ -724,76 +766,84 @@ Sanitizer.getClearRange = function (ts) 
 };
 
 Sanitizer._prefs = null;
 Sanitizer.__defineGetter__("prefs", function()
 {
   return Sanitizer._prefs ? Sanitizer._prefs
     : Sanitizer._prefs = Components.classes["@mozilla.org/preferences-service;1"]
                          .getService(Components.interfaces.nsIPrefService)
-                         .getBranch(Sanitizer.prefDomain);
+                         .getBranch(Sanitizer.PREF_DOMAIN);
 });
 
 // Shows sanitization UI
 Sanitizer.showUI = function(aParentWindow)
 {
   var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
                      .getService(Components.interfaces.nsIWindowWatcher);
-#ifdef XP_MACOSX
-  ww.openWindow(null, // make this an app-modal window on Mac
-#else
-  ww.openWindow(aParentWindow,
-#endif
+  let win = AppConstants.platform == "macosx" ?
+    null: // make this an app-modal window on Mac
+    aParentWindow;
+  ww.openWindow(win,
                 "chrome://browser/content/sanitize.xul",
                 "Sanitize",
                 "chrome,titlebar,dialog,centerscreen,modal",
                 null);
 };
 
 /**
  * Deletes privacy sensitive data in a batch, optionally showing the
  * sanitize UI, according to user preferences
  */
 Sanitizer.sanitize = function(aParentWindow)
 {
   Sanitizer.showUI(aParentWindow);
 };
 
-Sanitizer.onStartup = function()
-{
-  // we check for unclean exit with pending sanitization
-  Sanitizer._checkAndSanitize();
-};
+Sanitizer.onStartup = Task.async(function*() {
+  // Make sure that we are triggered during shutdown, at the right time.
+  let shutdownClient = Cc["@mozilla.org/browser/nav-history-service;1"]
+     .getService(Ci.nsPIPlacesDatabase)
+     .shutdownClient
+     .jsclient;
 
-Sanitizer.onShutdown = function()
-{
-  // we check if sanitization is needed and perform it
-  Sanitizer._checkAndSanitize();
-};
-
-// this is called on startup and shutdown, to perform pending sanitizations
-Sanitizer._checkAndSanitize = function()
-{
-  const prefs = Sanitizer.prefs;
-  if (prefs.getBoolPref(Sanitizer.prefShutdown) &&
-      !prefs.prefHasUserValue(Sanitizer.prefDidShutdown)) {
+  shutdownClient.addBlocker("sanitize.js: Sanitize on shutdown",
+    () => Sanitizer.onShutdown());
 
     // One time migration to remove support for the clear saved passwords on exit feature.
     if (!Services.prefs.getBoolPref("privacy.sanitize.migrateClearSavedPwdsOnExit")) {
       let deprecatedPref = "privacy.clearOnShutdown.passwords";
       let doUpdate = Services.prefs.prefHasUserValue(deprecatedPref) &&
                      Services.prefs.getBoolPref(deprecatedPref);
       if (doUpdate) {
         Services.logins.removeAllLogins();
         Services.prefs.setBoolPref("signon.rememberSignons", false);
       }
       Services.prefs.clearUserPref(deprecatedPref);
       Services.prefs.setBoolPref("privacy.sanitize.migrateClearSavedPwdsOnExit", true);
-    }
+  }
 
-    // this is a shutdown or a startup after an unclean exit
-    var s = new Sanitizer();
-    s.prefDomain = "privacy.clearOnShutdown.";
-    s.sanitize().then(function() {
-      prefs.setBoolPref(Sanitizer.prefDidShutdown, true);
-    });
+  // Handle incomplete sanitizations
+  if (Preferences.has(Sanitizer.PREF_SANITIZE_IN_PROGRESS)) {
+    // Firefox crashed during sanitization.
+    let s = new Sanitizer();
+    let json = Preferences.get(Sanitizer.PREF_SANITIZE_IN_PROGRESS);
+    let itemsToClear = JSON.parse(json);
+    yield s.sanitize(itemsToClear);
   }
-};
+  if (Preferences.has(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN)) {
+    // Firefox crashed before having a chance to sanitize during shutdown.
+    // (note that if Firefox crashed during shutdown sanitization, we
+    // will hit both `if` so we will run a second double-sanitization).
+    yield Sanitizer.onShutdown();
+  }
+});
+
+Sanitizer.onShutdown = Task.async(function*() {
+  if (!Preferences.get(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN)) {
+    return;
+  }
+  // Need to sanitize upon shutdown
+  let s = new Sanitizer();
+  s.prefDomain = "privacy.clearOnShutdown.";
+  yield s.sanitize();
+  Preferences.set(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN, true);
+});
--- a/browser/base/content/sanitizeDialog.js
+++ b/browser/base/content/sanitizeDialog.js
@@ -1,15 +1,18 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* 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 Cc = Components.classes;
 const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+let {Sanitizer} = Cu.import("resource:///modules/Sanitizer.jsm", {});
 
 var gSanitizePromptDialog = {
 
   get bundleBrowser()
   {
     if (!this._bundleBrowser)
       this._bundleBrowser = document.getElementById("bundleBrowser");
     return this._bundleBrowser;
@@ -38,40 +41,47 @@ var gSanitizePromptDialog = {
   init: function ()
   {
     // This is used by selectByTimespan() to determine if the window has loaded.
     this._inited = true;
 
     var s = new Sanitizer();
     s.prefDomain = "privacy.cpd.";
 
+    let tasks = [];
     let sanitizeItemList = document.querySelectorAll("#itemList > [preference]");
     for (let i = 0; i < sanitizeItemList.length; i++) {
       let prefItem = sanitizeItemList[i];
       let name = s.getNameFromPreference(prefItem.getAttribute("preference"));
-      s.canClearItem(name, function canClearCallback(aItem, aCanClear, aPrefItem) {
-        if (!aCanClear) {
-          aPrefItem.preference = null;
-          aPrefItem.checked = false;
-          aPrefItem.disabled = true;
+      let promise = s.promiseCanClearItem(name).then(canClear => {
+        if (canClear) {
+          return;
         }
-      }, prefItem);
+        prefItem.preference = null;
+        prefItem.checked = false;
+        prefItem.disabled = true;
+      });
+      tasks.push(promise);
     }
 
     document.documentElement.getButton("accept").label =
       this.bundleBrowser.getString("sanitizeButtonOK");
 
     if (this.selectedTimespan === Sanitizer.TIMESPAN_EVERYTHING) {
       this.prepareWarning();
       this.warningBox.hidden = false;
       document.title =
         this.bundleBrowser.getString("sanitizeDialog2.everything.title");
     }
     else
       this.warningBox.hidden = true;
+
+    Promise.all(tasks).then(() => {
+      Services.obs.notifyObservers(null, "sanitize-dialog-setup-complete", "");
+    });
   },
 
   selectByTimespan: function ()
   {
     // This method is the onselect handler for the duration dropdown.  As a
     // result it's called a couple of times before onload calls init().
     if (!this._inited)
       return;
@@ -114,25 +124,25 @@ var gSanitizePromptDialog = {
     // once the async operation completes (either with or without errors)
     // we close the window.
     let docElt = document.documentElement;
     let acceptButton = docElt.getButton("accept");
     acceptButton.disabled = true;
     acceptButton.setAttribute("label",
                               this.bundleBrowser.getString("sanitizeButtonClearing"));
     docElt.getButton("cancel").disabled = true;
+
     try {
       s.sanitize().then(null, Components.utils.reportError)
                   .then(() => window.close())
                   .then(null, Components.utils.reportError);
     } catch (er) {
       Components.utils.reportError("Exception during sanitize: " + er);
       return true; // We *do* want to close immediately on error.
     }
-    return false;
   },
 
   /**
    * If the panel that displays a warning when the duration is "Everything" is
    * not set up, sets it up.  Otherwise does nothing.
    *
    * @param aDontShowItemList Whether only the warning message should be updated.
    *                          True means the item list visibility status should not
@@ -500,17 +510,17 @@ var gSanitizePromptDialog = {
         }
       }
       // Otherwise use the predetermined range.
       else
         s.range = [this.durationStartTimes[durValue], Date.now() * 1000];
     }
 
     try {
-      s.sanitize();
+      s.sanitize(); // We ignore the resulting Promise
     } catch (er) {
       Components.utils.reportError("Exception during sanitize: " + er);
     }
     return true;
   },
 
   /**
    * In order to mark the custom Places tree view and its nsINavHistoryResult
--- a/browser/base/content/test/general/browser_sanitize-passwordDisabledHosts.js
+++ b/browser/base/content/test/general/browser_sanitize-passwordDisabledHosts.js
@@ -1,23 +1,21 @@
 // Bug 474792 - Clear "Never remember passwords for this site" when
 // clearing site-specific settings in Clear Recent History dialog
 
 var tempScope = {};
 Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader)
                                            .loadSubScript("chrome://browser/content/sanitize.js", tempScope);
 var Sanitizer = tempScope.Sanitizer;
 
-function test() {
-
+add_task(function*() {
   var pwmgr = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
 
   // Add a disabled host
   pwmgr.setLoginSavingEnabled("http://example.com", false);
-  
   // Sanity check
   is(pwmgr.getLoginSavingEnabled("http://example.com"), false,
      "example.com should be disabled for password saving since we haven't cleared that yet.");
 
   // Set up the sanitizer to just clear siteSettings
   let s = new Sanitizer();
   s.ignoreTimespan = false;
   s.prefDomain = "privacy.cpd.";
@@ -26,16 +24,16 @@ function test() {
   itemPrefs.setBoolPref("downloads", false);
   itemPrefs.setBoolPref("cache", false);
   itemPrefs.setBoolPref("cookies", false);
   itemPrefs.setBoolPref("formdata", false);
   itemPrefs.setBoolPref("offlineApps", false);
   itemPrefs.setBoolPref("passwords", false);
   itemPrefs.setBoolPref("sessions", false);
   itemPrefs.setBoolPref("siteSettings", true);
-  
+
   // Clear it
-  s.sanitize();
-  
+  yield s.sanitize();
+
   // Make sure it's gone
   is(pwmgr.getLoginSavingEnabled("http://example.com"), true,
      "example.com should be enabled for password saving again now that we've cleared.");
-}
+});
--- a/browser/base/content/test/general/browser_sanitize-sitepermissions.js
+++ b/browser/base/content/test/general/browser_sanitize-sitepermissions.js
@@ -10,17 +10,17 @@ function countPermissions() {
   let enumerator = Services.perms.enumerator;
   while (enumerator.hasMoreElements()) {
     result++;
     enumerator.getNext();
   }
   return result;
 }
 
-function test() {
+add_task(function* test() {
   // sanitize before we start so we have a good baseline.
   // Set up the sanitizer to just clear siteSettings
   let s = new Sanitizer();
   s.ignoreTimespan = false;
   s.prefDomain = "privacy.cpd.";
   var itemPrefs = gPrefService.getBranch(s.prefDomain);
   itemPrefs.setBoolPref("history", false);
   itemPrefs.setBoolPref("downloads", false);
@@ -40,13 +40,13 @@ function test() {
   // Add a permission entry
   var pm = Services.perms;
   pm.add(makeURI("http://example.com"), "testing", pm.ALLOW_ACTION);
 
   // Sanity check
   ok(pm.enumerator.hasMoreElements(), "Permission manager should have elements, since we just added one");
 
   // Clear it
-  s.sanitize();
+  yield s.sanitize();
 
   // Make sure it's gone
   is(numAtStart, countPermissions(), "Permission manager should have the same count it started with");
-}
+});
--- a/browser/base/content/test/general/browser_sanitizeDialog.js
+++ b/browser/base/content/test/general/browser_sanitizeDialog.js
@@ -13,225 +13,222 @@
  * test checks the UI of the dialog and makes sure it's correctly connected to
  * the sanitize timespan code.
  *
  * Some of this code, especially the history creation parts, was taken from
  * browser/base/content/test/general/browser_sanitize-timespans.js.
  */
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
-var {LoadContextInfo} = Cu.import("resource://gre/modules/LoadContextInfo.jsm", {});
+let {LoadContextInfo} = Cu.import("resource://gre/modules/LoadContextInfo.jsm", {});
 
 XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
                                   "resource://gre/modules/FormHistory.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
                                   "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Timer",
+                                  "resource://gre/modules/Timer.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
+                                  "resource://testing-common/PlacesTestUtils.jsm");
 
-var tempScope = {};
+let tempScope = {};
 Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader)
                                            .loadSubScript("chrome://browser/content/sanitize.js", tempScope);
-var Sanitizer = tempScope.Sanitizer;
+let Sanitizer = tempScope.Sanitizer;
 
 const kMsecPerMin = 60 * 1000;
 const kUsecPerMin = 60 * 1000000;
 
-var formEntries, downloadIDs, olderDownloadIDs;
-
-// Add tests here.  Each is a function that's called by doNextTest().
-var gAllTests = [
+add_task(function* init() {
+  requestLongerTimeout(2);
+  blankSlate();
+  registerCleanupFunction(() => {
+    blankSlate();
+    return PlacesTestUtils.promiseAsyncUpdates();
+  });
+});
 
-  /**
-   * Initializes the dialog to its default state.
-   */
-  function () {
+/**
+ * Initializes the dialog to its default state.
+ */
+add_task(function* default_state() {
+  let wh = new WindowHelper();
+  wh.onload = function () {
+    // Select "Last Hour"
+    this.selectDuration(Sanitizer.TIMESPAN_HOUR);
+    // Hide details
+    if (!this.getItemList().collapsed)
+      this.toggleDetails();
+    this.acceptDialog();
+  };
+  wh.open();
+  return wh.promiseClosed;
+});
+
+/**
+ * Cancels the dialog, makes sure history not cleared.
+ */
+add_task(function* test_cancel() {
+  // Add history (within the past hour)
+  let uris = [];
+  let places = [];
+  let pURI;
+  for (let i = 0; i < 30; i++) {
+    pURI = makeURI("http://" + i + "-minutes-ago.com/");
+    places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)});
+    uris.push(pURI);
+  }
+
+  return new Promise(resolve => {
+    PlacesTestUtils.addVisits(places).then(() => {
     let wh = new WindowHelper();
     wh.onload = function () {
-      // Select "Last Hour"
       this.selectDuration(Sanitizer.TIMESPAN_HOUR);
-      // Hide details
-      if (!this.getItemList().collapsed)
-        this.toggleDetails();
-      this.acceptDialog();
-    };
-    wh.open();
-  },
+      this.checkPrefCheckbox("history", false);
+      this.checkDetails(false);
+
+      // Show details
+      this.toggleDetails();
+      this.checkDetails(true);
 
-  /**
-   * Cancels the dialog, makes sure history not cleared.
-   */
-  function () {
-    // Add history (within the past hour)
-    let uris = [];
-    let places = [];
-    let pURI;
-    for (let i = 0; i < 30; i++) {
-      pURI = makeURI("http://" + i + "-minutes-ago.com/");
-      places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)});
-      uris.push(pURI);
-    }
-
-    PlacesTestUtils.addVisits(places).then(() => {
-      let wh = new WindowHelper();
-      wh.onload = function () {
-        this.selectDuration(Sanitizer.TIMESPAN_HOUR);
-        this.checkPrefCheckbox("history", false);
-        this.checkDetails(false);
-
-        // Show details
-        this.toggleDetails();
-        this.checkDetails(true);
+      // Hide details
+      this.toggleDetails();
+      this.checkDetails(false);
+      this.cancelDialog();
+    };
+    wh.onunload = function* () {
+      yield promiseHistoryClearedState(uris, false);
+      yield blankSlate();
+      yield promiseHistoryClearedState(uris, true);
+    };
+    wh.promiseClosed.then(resolve);
+    wh.open();
+  })});
+});
 
-        // Hide details
-        this.toggleDetails();
-        this.checkDetails(false);
-        this.cancelDialog();
-      };
-      wh.onunload = function () {
-        yield promiseHistoryClearedState(uris, false);
-        yield blankSlate();
-        yield promiseHistoryClearedState(uris, true);
-      };
-      wh.open();
-    });
-  },
-
-  function () {
-    // Add downloads (within the past hour).
-    Task.spawn(function () {
-      downloadIDs = [];
-      for (let i = 0; i < 5; i++) {
-        yield addDownloadWithMinutesAgo(downloadIDs, i);
-      }
-      // Add downloads (over an hour ago).
-      olderDownloadIDs = [];
-      for (let i = 0; i < 5; i++) {
-        yield addDownloadWithMinutesAgo(olderDownloadIDs, 61 + i);
-      }
+/**
+ * Ensures that the combined history-downloads checkbox clears both history
+ * visits and downloads when checked; the dialog respects simple timespan.
+ */
+add_task(function* test_history_downloads_checked() {
+  // Add downloads (within the past hour).
+  let downloadIDs = [];
+  for (let i = 0; i < 5; i++) {
+    yield addDownloadWithMinutesAgo(downloadIDs, i);
+  }
+  // Add downloads (over an hour ago).
+  let olderDownloadIDs = [];
+  for (let i = 0; i < 5; i++) {
+    yield addDownloadWithMinutesAgo(olderDownloadIDs, 61 + i);
+  }
 
-      doNextTest();
-    }).then(null, Components.utils.reportError);
-  },
+  // Add history (within the past hour).
+  let uris = [];
+  let places = [];
+  let pURI;
+  for (let i = 0; i < 30; i++) {
+    pURI = makeURI("http://" + i + "-minutes-ago.com/");
+    places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)});
+    uris.push(pURI);
+  }
+  // Add history (over an hour ago).
+  let olderURIs = [];
+  for (let i = 0; i < 5; i++) {
+    pURI = makeURI("http://" + (61 + i) + "-minutes-ago.com/");
+    places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(61 + i)});
+    olderURIs.push(pURI);
+  }
+  let promiseSanitized = promiseSanitizationComplete();
 
-  /**
-   * Ensures that the combined history-downloads checkbox clears both history
-   * visits and downloads when checked; the dialog respects simple timespan.
-   */
-  function () {
-    // Add history (within the past hour).
-    let uris = [];
-    let places = [];
-    let pURI;
-    for (let i = 0; i < 30; i++) {
-      pURI = makeURI("http://" + i + "-minutes-ago.com/");
-      places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)});
-      uris.push(pURI);
-    }
-    // Add history (over an hour ago).
-    let olderURIs = [];
-    for (let i = 0; i < 5; i++) {
-      pURI = makeURI("http://" + (61 + i) + "-minutes-ago.com/");
-      places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(61 + i)});
-      olderURIs.push(pURI);
-    }
-
+  return new Promise(resolve => {
     PlacesTestUtils.addVisits(places).then(() => {
       let totalHistoryVisits = uris.length + olderURIs.length;
 
       let wh = new WindowHelper();
       wh.onload = function () {
         this.selectDuration(Sanitizer.TIMESPAN_HOUR);
         this.checkPrefCheckbox("history", true);
         this.acceptDialog();
       };
-      wh.onunload = function () {
+      wh.onunload = function* () {
         intPrefIs("sanitize.timeSpan", Sanitizer.TIMESPAN_HOUR,
                   "timeSpan pref should be hour after accepting dialog with " +
                   "hour selected");
         boolPrefIs("cpd.history", true,
                    "history pref should be true after accepting dialog with " +
                    "history checkbox checked");
         boolPrefIs("cpd.downloads", true,
                    "downloads pref should be true after accepting dialog with " +
                    "history checkbox checked");
 
+        yield promiseSanitized;
+
         // History visits and downloads within one hour should be cleared.
         yield promiseHistoryClearedState(uris, true);
         yield ensureDownloadsClearedState(downloadIDs, true);
 
         // Visits and downloads > 1 hour should still exist.
         yield promiseHistoryClearedState(olderURIs, false);
         yield ensureDownloadsClearedState(olderDownloadIDs, false);
 
         // OK, done, cleanup after ourselves.
         yield blankSlate();
         yield promiseHistoryClearedState(olderURIs, true);
         yield ensureDownloadsClearedState(olderDownloadIDs, true);
       };
+      wh.promiseClosed.then(resolve);
       wh.open();
     });
-  },
-
-  /**
-   * Add form history entries for the next test.
-   */
-  function () {
-    formEntries = [];
+  });
+});
 
-    let iter = function() {
-      for (let i = 0; i < 5; i++) {
-        formEntries.push(addFormEntryWithMinutesAgo(iter, i));
-        yield undefined;
-      }
-      doNextTest();
-    }();
+/**
+ * Ensures that the combined history-downloads checkbox removes neither
+ * history visits nor downloads when not checked.
+ */
+add_task(function* test_history_downloads_unchecked() {
+  // Add form entries
+  let formEntries = [];
 
-    iter.next();
-  },
+  for (let i = 0; i < 5; i++) {
+    formEntries.push((yield promiseAddFormEntryWithMinutesAgo(i)));
+  }
+
 
-  function () {
-    // Add downloads (within the past hour).
-    Task.spawn(function () {
-      downloadIDs = [];
-      for (let i = 0; i < 5; i++) {
-        yield addDownloadWithMinutesAgo(downloadIDs, i);
-      }
-
-      doNextTest();
-    }).then(null, Components.utils.reportError);
-  },
+  // Add downloads (within the past hour).
+  let downloadIDs = [];
+  for (let i = 0; i < 5; i++) {
+    yield addDownloadWithMinutesAgo(downloadIDs, i);
+  }
 
-  /**
-   * Ensures that the combined history-downloads checkbox removes neither
-   * history visits nor downloads when not checked.
-   */
-  function () {
-    // Add history, downloads, form entries (within the past hour).
-    let uris = [];
-    let places = [];
-    let pURI;
-    for (let i = 0; i < 5; i++) {
-      pURI = makeURI("http://" + i + "-minutes-ago.com/");
-      places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)});
-      uris.push(pURI);
-    }
+  // Add history, downloads, form entries (within the past hour).
+  let uris = [];
+  let places = [];
+  let pURI;
+  for (let i = 0; i < 5; i++) {
+    pURI = makeURI("http://" + i + "-minutes-ago.com/");
+    places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)});
+    uris.push(pURI);
+  }
 
+  return new Promise(resolve => {
     PlacesTestUtils.addVisits(places).then(() => {
       let wh = new WindowHelper();
       wh.onload = function () {
         is(this.isWarningPanelVisible(), false,
            "Warning panel should be hidden after previously accepting dialog " +
            "with a predefined timespan");
         this.selectDuration(Sanitizer.TIMESPAN_HOUR);
 
         // Remove only form entries, leave history (including downloads).
         this.checkPrefCheckbox("history", false);
         this.checkPrefCheckbox("formdata", true);
         this.acceptDialog();
       };
-      wh.onunload = function () {
+      wh.onunload = function* () {
         intPrefIs("sanitize.timeSpan", Sanitizer.TIMESPAN_HOUR,
                   "timeSpan pref should be hour after accepting dialog with " +
                   "hour selected");
         boolPrefIs("cpd.history", false,
                    "history pref should be false after accepting dialog with " +
                    "history checkbox unchecked");
         boolPrefIs("cpd.downloads", false,
                    "downloads pref should be false after accepting dialog with " +
@@ -246,35 +243,41 @@ var gAllTests = [
           is(exists, false, "form entry " + entry + " should no longer exist");
         });
 
         // OK, done, cleanup after ourselves.
         yield blankSlate();
         yield promiseHistoryClearedState(uris, true);
         yield ensureDownloadsClearedState(downloadIDs, true);
       };
+      wh.promiseClosed.then(resolve);
       wh.open();
     });
-  },
+  });
+});
 
-  /**
-   * Ensures that the "Everything" duration option works.
-   */
-  function () {
-    // Add history.
-    let uris = [];
-    let places = [];
-    let pURI;
-    // within past hour, within past two hours, within past four hours and 
-    // outside past four hours
-    [10, 70, 130, 250].forEach(function(aValue) {
-      pURI = makeURI("http://" + aValue + "-minutes-ago.com/");
-      places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(aValue)});
-      uris.push(pURI);
-    });
+/**
+ * Ensures that the "Everything" duration option works.
+ */
+add_task(function* test_everything() {
+  // Add history.
+  let uris = [];
+  let places = [];
+  let pURI;
+  // within past hour, within past two hours, within past four hours and 
+  // outside past four hours
+  [10, 70, 130, 250].forEach(function(aValue) {
+    pURI = makeURI("http://" + aValue + "-minutes-ago.com/");
+    places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(aValue)});
+    uris.push(pURI);
+  });
+
+  let promiseSanitized = promiseSanitizationComplete();
+
+  return new Promise(resolve => {
     PlacesTestUtils.addVisits(places).then(() => {
       let wh = new WindowHelper();
       wh.onload = function () {
         is(this.isWarningPanelVisible(), false,
            "Warning panel should be hidden after previously accepting dialog " +
            "with a predefined timespan");
         this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING);
         this.checkPrefCheckbox("history", true);
@@ -286,84 +289,88 @@ var gAllTests = [
 
         // Show details
         this.toggleDetails();
         this.checkDetails(true);
 
         this.acceptDialog();
       };
       wh.onunload = function () {
+        yield promiseSanitized;
         intPrefIs("sanitize.timeSpan", Sanitizer.TIMESPAN_EVERYTHING,
                   "timeSpan pref should be everything after accepting dialog " +
                   "with everything selected");
 
         yield promiseHistoryClearedState(uris, true);
       };
+      wh.promiseClosed.then(resolve);
       wh.open();
     });
-  },
+  });
+});
 
-  /**
-   * Ensures that the "Everything" warning is visible on dialog open after
-   * the previous test.
-   */
-  function () {
-    // Add history.
-    let uris = [];
-    let places = [];
-    let pURI;
-    // within past hour, within past two hours, within past four hours and 
-    // outside past four hours
-    [10, 70, 130, 250].forEach(function(aValue) {
-      pURI = makeURI("http://" + aValue + "-minutes-ago.com/");
-      places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(aValue)});
-      uris.push(pURI);
-    });
+/**
+ * Ensures that the "Everything" warning is visible on dialog open after
+ * the previous test.
+ */
+add_task(function* test_everything_warning() {
+  // Add history.
+  let uris = [];
+  let places = [];
+  let pURI;
+  // within past hour, within past two hours, within past four hours and 
+  // outside past four hours
+  [10, 70, 130, 250].forEach(function(aValue) {
+    pURI = makeURI("http://" + aValue + "-minutes-ago.com/");
+    places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(aValue)});
+    uris.push(pURI);
+  });
+
+  let promiseSanitized = promiseSanitizationComplete();
+
+  return new Promise(resolve => {
     PlacesTestUtils.addVisits(places).then(() => {
       let wh = new WindowHelper();
       wh.onload = function () {
         is(this.isWarningPanelVisible(), true,
            "Warning panel should be visible after previously accepting dialog " +
            "with clearing everything");
         this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING);
         this.checkPrefCheckbox("history", true);
         this.acceptDialog();
       };
       wh.onunload = function () {
         intPrefIs("sanitize.timeSpan", Sanitizer.TIMESPAN_EVERYTHING,
                   "timeSpan pref should be everything after accepting dialog " +
                   "with everything selected");
 
+        yield promiseSanitized;
+
         yield promiseHistoryClearedState(uris, true);
       };
+      wh.promiseClosed.then(resolve);
       wh.open();
     });
-  },
-
-  /**
-   * Add form history entry for the next test.
-   */
-  function () {
-    let iter = function() {
-      formEntries = [ addFormEntryWithMinutesAgo(iter, 10) ];
-      yield undefined;
-      doNextTest();
-    }();
+  });
+});
 
-    iter.next();
-  },
+/**
+ * The next three tests checks that when a certain history item cannot be
+ * cleared then the checkbox should be both disabled and unchecked.
+ * In addition, we ensure that this behavior does not modify the preferences.
+ */
+add_task(function* test_cannot_clear_history() {
+  // Add form entries
+  let formEntries = [ (yield promiseAddFormEntryWithMinutesAgo(10)) ];
 
-  /**
-   * The next three tests checks that when a certain history item cannot be
-   * cleared then the checkbox should be both disabled and unchecked.
-   * In addition, we ensure that this behavior does not modify the preferences.
-   */
-  function () {
-    // Add history.
-    let pURI = makeURI("http://" + 10 + "-minutes-ago.com/");
+  let promiseSanitized = promiseSanitizationComplete();
+
+  // Add history.
+  let pURI = makeURI("http://" + 10 + "-minutes-ago.com/");
+  return new Promise(resolve => {
     PlacesTestUtils.addVisits({uri: pURI, visitDate: visitTimeForMinutesAgo(10)}).then(() => {
       let uris = [ pURI ];
 
       let wh = new WindowHelper();
       wh.onload = function() {
         // Check that the relevant checkboxes are enabled
         var cb = this.win.document.querySelectorAll(
                    "#itemList > [preference='privacy.cpd.formdata']");
@@ -374,312 +381,320 @@ var gAllTests = [
                    "#itemList > [preference='privacy.cpd.history']");
         ok(cb.length == 1 && !cb[0].disabled, "There is history, checkbox to " +
            "clear history should be enabled.");
 
         this.checkAllCheckboxes();
         this.acceptDialog();
       };
       wh.onunload = function () {
+        yield promiseSanitized;
+
         yield promiseHistoryClearedState(uris, true);
 
         let exists = yield formNameExists(formEntries[0]);
         is(exists, false, "form entry " + formEntries[0] + " should no longer exist");
       };
+      wh.promiseClosed.then(resolve);
       wh.open();
     });
-  },
-  function () {
-    let wh = new WindowHelper();
-    wh.onload = function() {
-      boolPrefIs("cpd.history", true,
-                 "history pref should be true after accepting dialog with " +
-                 "history checkbox checked");
-      boolPrefIs("cpd.formdata", true,
-                 "formdata pref should be true after accepting dialog with " +
-                 "formdata checkbox checked");
+  });
+});
+
+add_task(function* test_no_formdata_history_to_clear() {
+  let promiseSanitized = promiseSanitizationComplete();
+  let wh = new WindowHelper();
+  wh.onload = function() {
+    boolPrefIs("cpd.history", true,
+               "history pref should be true after accepting dialog with " +
+               "history checkbox checked");
+    boolPrefIs("cpd.formdata", true,
+               "formdata pref should be true after accepting dialog with " +
+               "formdata checkbox checked");
+
+    // Even though the formdata pref is true, because there is no history
+    // left to clear, the checkbox will be disabled.
+    var cb = this.win.document.querySelectorAll(
+               "#itemList > [preference='privacy.cpd.formdata']");
+    ok(cb.length == 1 && cb[0].disabled && !cb[0].checked,
+       "There is no formdata history, checkbox should be disabled and be " +
+       "cleared to reduce user confusion (bug 497664).");
+
+    var cb = this.win.document.querySelectorAll(
+               "#itemList > [preference='privacy.cpd.history']");
+    ok(cb.length == 1 && !cb[0].disabled && cb[0].checked,
+       "There is no history, but history checkbox should always be enabled " +
+       "and will be checked from previous preference.");
+
+    this.acceptDialog();
+  }
+  wh.open();
+  yield wh.promiseClosed;
+  yield promiseSanitized;
+});
+
+add_task(function* test_form_entries() {
+  let formEntry = (yield promiseAddFormEntryWithMinutesAgo(10));
+
+  let promiseSanitized = promiseSanitizationComplete();
+
+  let wh = new WindowHelper();
+  wh.onload = function() {
+    boolPrefIs("cpd.formdata", true,
+               "formdata pref should persist previous value after accepting " +
+               "dialog where you could not clear formdata.");
+
+    var cb = this.win.document.querySelectorAll(
+               "#itemList > [preference='privacy.cpd.formdata']");
+
+    info("There exists formEntries so the checkbox should be in sync with the pref.");
+    is(cb.length, 1, "There is only one checkbox for form data");
+    ok(!cb[0].disabled, "The checkbox is enabled");
+    ok(cb[0].checked, "The checkbox is checked");
+
+    this.acceptDialog();
+  };
+  wh.onunload = function () {
+    yield promiseSanitized;
+    let exists = yield formNameExists(formEntry);
+    is(exists, false, "form entry " + formEntry + " should no longer exist");
+  };
+  wh.open();
+  return wh.promiseClosed;
+});
 
 
-      // Even though the formdata pref is true, because there is no history
-      // left to clear, the checkbox will be disabled.
-      var cb = this.win.document.querySelectorAll(
-                 "#itemList > [preference='privacy.cpd.formdata']");
-
-      // Wait until the checkbox is disabled. This is done asynchronously
-      // from Sanitizer.init() as FormHistory.count() is a purely async API.
-      promiseWaitForCondition(() => cb[0].disabled).then(() => {
-        ok(cb.length == 1 && cb[0].disabled && !cb[0].checked,
-           "There is no formdata history, checkbox should be disabled and be " +
-           "cleared to reduce user confusion (bug 497664).");
-
-        cb = this.win.document.querySelectorAll(
-                   "#itemList > [preference='privacy.cpd.history']");
-        ok(cb.length == 1 && !cb[0].disabled && cb[0].checked,
-           "There is no history, but history checkbox should always be enabled " +
-           "and will be checked from previous preference.");
-
-        this.acceptDialog();
-      });
-    }
-    wh.open();
-  },
-
-  /**
-   * Add form history entry for the next test.
-   */
-  function () {
-    let iter = function() {
-      formEntries = [ addFormEntryWithMinutesAgo(iter, 10) ];
-      yield undefined;
-      doNextTest();
-    }();
-
-    iter.next();
-  },
-
-  function () {
-    let wh = new WindowHelper();
-    wh.onload = function() {
-      boolPrefIs("cpd.formdata", true,
-                 "formdata pref should persist previous value after accepting " +
-                 "dialog where you could not clear formdata.");
-
-      var cb = this.win.document.querySelectorAll(
-                 "#itemList > [preference='privacy.cpd.formdata']");
-      ok(cb.length == 1 && !cb[0].disabled && cb[0].checked,
-         "There exists formEntries so the checkbox should be in sync with " +
-         "the pref.");
-
-      this.acceptDialog();
-    };
-    wh.onunload = function () {
-      let exists = yield formNameExists(formEntries[0]);
-      is(exists, false, "form entry " + formEntries[0] + " should no longer exist");
-    };
-    wh.open();
-  },
-
-
-  /**
-   * These next six tests together ensure that toggling details persists
-   * across dialog openings.
-   */
-  function () {
+/**
+ * Ensure that toggling details persists
+ * across dialog openings.
+ */
+add_task(function* test_toggling_details_persists() {
+  {
     let wh = new WindowHelper();
     wh.onload = function () {
       // Check all items and select "Everything"
       this.checkAllCheckboxes();
       this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING);
 
       // Hide details
       this.toggleDetails();
       this.checkDetails(false);
       this.acceptDialog();
     };
     wh.open();
-  },
-  function () {
+    yield wh.promiseClosed;
+  }
+  {
     let wh = new WindowHelper();
     wh.onload = function () {
       // Details should remain closed because all items are checked.
       this.checkDetails(false);
 
       // Uncheck history.
       this.checkPrefCheckbox("history", false);
       this.acceptDialog();
     };
     wh.open();
-  },
-  function () {
+    yield wh.promiseClosed;
+  }
+  {
     let wh = new WindowHelper();
     wh.onload = function () {
       // Details should be open because not all items are checked.
       this.checkDetails(true);
 
       // Modify the Site Preferences item state (bug 527820)
       this.checkAllCheckboxes();
       this.checkPrefCheckbox("siteSettings", false);
       this.acceptDialog();
     };
     wh.open();
-  },
-  function () {
+    yield wh.promiseClosed;
+  }
+  {
     let wh = new WindowHelper();
     wh.onload = function () {
       // Details should be open because not all items are checked.
       this.checkDetails(true);
 
       // Hide details
       this.toggleDetails();
       this.checkDetails(false);
       this.cancelDialog();
     };
     wh.open();
-  },
-  function () {
+    yield wh.promiseClosed;
+  }
+  {
     let wh = new WindowHelper();
     wh.onload = function () {
       // Details should be open because not all items are checked.
       this.checkDetails(true);
 
       // Select another duration
       this.selectDuration(Sanitizer.TIMESPAN_HOUR);
       // Hide details
       this.toggleDetails();
       this.checkDetails(false);
       this.acceptDialog();
     };
     wh.open();
-  },
-  function () {
+    yield wh.promiseClosed;
+  }
+  {
     let wh = new WindowHelper();
     wh.onload = function () {
       // Details should not be open because "Last Hour" is selected
       this.checkDetails(false);
 
       this.cancelDialog();
     };
     wh.open();
-  },
-  function () {
+    yield wh.promiseClosed;
+  }
+  {
     let wh = new WindowHelper();
     wh.onload = function () {
       // Details should have remained closed
       this.checkDetails(false);
 
       // Show details
       this.toggleDetails();
       this.checkDetails(true);
       this.cancelDialog();
     };
     wh.open();
-  },
-  function () {
-    // Test for offline cache deletion
+    yield wh.promiseClosed;
+  }
+});
+
+// Test for offline cache deletion
+add_task(function* test_offline_cache() {
+  // Prepare stuff, we will work with www.example.com
+  var URL = "http://www.example.com";
+
+  var ios = Cc["@mozilla.org/network/io-service;1"]
+            .getService(Ci.nsIIOService);
+  var URI = ios.newURI(URL, null, null);
 
-    // Prepare stuff, we will work with www.example.com
-    var URL = "http://www.example.com";
+  var sm = Cc["@mozilla.org/scriptsecuritymanager;1"]
+           .getService(Ci.nsIScriptSecurityManager);
+  var principal = sm.getNoAppCodebasePrincipal(URI);
+
+  // Give www.example.com privileges to store offline data
+  var pm = Cc["@mozilla.org/permissionmanager;1"]
+           .getService(Ci.nsIPermissionManager);
+  pm.addFromPrincipal(principal, "offline-app", Ci.nsIPermissionManager.ALLOW_ACTION);
+  pm.addFromPrincipal(principal, "offline-app", Ci.nsIOfflineCacheUpdateService.ALLOW_NO_WARN);
+
+  // Store something to the offline cache
+  var appcacheserv = Cc["@mozilla.org/network/application-cache-service;1"]
+                     .getService(Ci.nsIApplicationCacheService);
+  var appcachegroupid = appcacheserv.buildGroupID(makeURI(URL + "/manifest"), LoadContextInfo.default);
+  var appcache = appcacheserv.createApplicationCache(appcachegroupid);
+
+  var cacheserv = Cc["@mozilla.org/netwerk/cache-storage-service;1"]
+                  .getService(Ci.nsICacheStorageService);
+  var storage = cacheserv.appCacheStorage(LoadContextInfo.default, appcache);
 
-    var ios = Cc["@mozilla.org/network/io-service;1"]
-              .getService(Ci.nsIIOService);
-    var URI = ios.newURI(URL, null, null);
+  // Open the dialog
+  let wh = new WindowHelper();
+  wh.onload = function () {
+    this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING);
+    // Show details
+    this.toggleDetails();
+    // Clear only offlineApps
+    this.uncheckAllCheckboxes();
+    this.checkPrefCheckbox("offlineApps", true);
+    this.acceptDialog();
+  };
+  wh.onunload = function () {
+    // Check if the cache has been deleted
+    var size = -1;
+    var visitor = {
+      onCacheStorageInfo: function (aEntryCount, aConsumption, aCapacity, aDiskDirectory)
+      {
+        size = aConsumption;
+      }
+    };
+    storage.asyncVisitStorage(visitor, false);
+    // Offline cache visit happens synchronously, since it's forwarded to the old code
+    is(size, 0, "offline application cache entries evicted");
+  };
 
-    var sm = Cc["@mozilla.org/scriptsecuritymanager;1"]
-             .getService(Ci.nsIScriptSecurityManager);
-    var principal = sm.createCodebasePrincipal(URI, {});
+  var cacheListener = {
+    onCacheEntryCheck: function() { return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; },
+    onCacheEntryAvailable: function (entry, isnew, appcache, status) {
+      is(status, Cr.NS_OK);
+      var stream = entry.openOutputStream(0);
+      var content = "content";
+      stream.write(content, content.length);
+      stream.close();
+      entry.close();
+      wh.open();
+    }
+  };
+
+  storage.asyncOpenURI(makeURI(URL), "", Ci.nsICacheStorage.OPEN_TRUNCATE, cacheListener);
+  return wh.promiseClosed;
+});
+
+// Test for offline apps permission deletion
+add_task(function* test_offline_apps_permissions() {
+  // Prepare stuff, we will work with www.example.com
+  var URL = "http://www.example.com";
 
-    // Give www.example.com privileges to store offline data
+  var ios = Cc["@mozilla.org/network/io-service;1"]
+            .getService(Ci.nsIIOService);
+  var URI = ios.newURI(URL, null, null);
+
+  var sm = Cc["@mozilla.org/scriptsecuritymanager;1"]
+           .getService(Ci.nsIScriptSecurityManager);
+  var principal = sm.createCodebasePrincipal(URI, {});
+
+  let promiseSanitized = promiseSanitizationComplete();
+
+  // Open the dialog
+  let wh = new WindowHelper();
+  wh.onload = function () {
+    this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING);
+    // Show details
+    this.toggleDetails();
+    // Clear only offlineApps
+    this.uncheckAllCheckboxes();
+    this.checkPrefCheckbox("siteSettings", true);
+    this.acceptDialog();
+  };
+  wh.onunload = function () {
+    yield promiseSanitized;
+
+    // Check all has been deleted (privileges, data, cache)
     var pm = Cc["@mozilla.org/permissionmanager;1"]
              .getService(Ci.nsIPermissionManager);
-    pm.addFromPrincipal(principal, "offline-app", Ci.nsIPermissionManager.ALLOW_ACTION);
-    pm.addFromPrincipal(principal, "offline-app", Ci.nsIOfflineCacheUpdateService.ALLOW_NO_WARN);
-
-    // Store something to the offline cache
-    var appcacheserv = Cc["@mozilla.org/network/application-cache-service;1"]
-                       .getService(Ci.nsIApplicationCacheService);
-    var appcachegroupid = appcacheserv.buildGroupID(makeURI(URL + "/manifest"), LoadContextInfo.default);
-    var appcache = appcacheserv.createApplicationCache(appcachegroupid);
-
-    var cacheserv = Cc["@mozilla.org/netwerk/cache-storage-service;1"]
-                    .getService(Ci.nsICacheStorageService);
-    var storage = cacheserv.appCacheStorage(LoadContextInfo.default, appcache);
-
-    // Open the dialog
-    let wh = new WindowHelper();
-    wh.onload = function () {
-      this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING);
-      // Show details
-      this.toggleDetails();
-      // Clear only offlineApps
-      this.uncheckAllCheckboxes();
-      this.checkPrefCheckbox("offlineApps", true);
-      this.acceptDialog();
-    };
-    wh.onunload = function () {
-      // Check if the cache has been deleted
-      var size = -1;
-      var visitor = {
-        onCacheStorageInfo: function (aEntryCount, aConsumption, aCapacity, aDiskDirectory)
-        {
-          size = aConsumption;
-        }
-      };
-      storage.asyncVisitStorage(visitor, false);
-      // Offline cache visit happens synchronously, since it's forwarded to the old code
-      is(size, 0, "offline application cache entries evicted");
-    };
+    is(pm.testPermissionFromPrincipal(principal, "offline-app"), 0, "offline-app permissions removed");
+  };
+  wh.open();
+  return wh.promiseClosed;
+});
 
-    var cacheListener = {
-      onCacheEntryCheck: function() { return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; },
-      onCacheEntryAvailable: function (entry, isnew, appcache, status) {
-        is(status, Cr.NS_OK);
-        var stream = entry.openOutputStream(0);
-        var content = "content";
-        stream.write(content, content.length);
-        stream.close();
-        entry.close();
-        wh.open();
-      }
-    };
-
-    storage.asyncOpenURI(makeURI(URL), "", Ci.nsICacheStorage.OPEN_TRUNCATE, cacheListener);
-  },
-  function () {
-    // Test for offline apps permission deletion
-
-    // Prepare stuff, we will work with www.example.com
-    var URL = "http://www.example.com";
-
-    var ios = Cc["@mozilla.org/network/io-service;1"]
-              .getService(Ci.nsIIOService);
-    var URI = ios.newURI(URL, null, null);
-
-    var sm = Cc["@mozilla.org/scriptsecuritymanager;1"]
-             .getService(Ci.nsIScriptSecurityManager);
-    var principal = sm.createCodebasePrincipal(URI, {});
-
-    // Open the dialog
-    let wh = new WindowHelper();
-    wh.onload = function () {
-      this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING);
-      // Show details
-      this.toggleDetails();
-      // Clear only offlineApps
-      this.uncheckAllCheckboxes();
-      this.checkPrefCheckbox("siteSettings", true);
-      this.acceptDialog();
-    };
-    wh.onunload = function () {
-      // Check all has been deleted (privileges, data, cache)
-      var pm = Cc["@mozilla.org/permissionmanager;1"]
-               .getService(Ci.nsIPermissionManager);
-      is(pm.testPermissionFromPrincipal(principal, "offline-app"), 0, "offline-app permissions removed");
-    };
-    wh.open();
-  }
-];
-
-// Index in gAllTests of the test currently being run.  Incremented for each
-// test run.  See doNextTest().
-var gCurrTest = 0;
-
-var now_mSec = Date.now();
-var now_uSec = now_mSec * 1000;
+let now_mSec = Date.now();
+let now_uSec = now_mSec * 1000;
 
 ///////////////////////////////////////////////////////////////////////////////
 
 /**
  * This wraps the dialog and provides some convenience methods for interacting
  * with it.
  *
  * @param aWin
  *        The dialog's nsIDOMWindow
  */
 function WindowHelper(aWin) {
   this.win = aWin;
+  this.promiseClosed = new Promise(resolve => {this._resolveClosed = resolve});
 }
 
 WindowHelper.prototype = {
   /**
    * "Presses" the dialog's OK button.
    */
   acceptDialog: function () {
     is(this.win.document.documentElement.getButton("accept").disabled, false,
@@ -814,69 +829,55 @@ WindowHelper.prototype = {
       if (aTopic != "domwindowopened")
         return;
 
       Services.ww.unregisterNotification(windowObserver);
 
       var loaded = false;
       let win = aSubject.QueryInterface(Ci.nsIDOMWindow);
 
+      let promiseDialogReady = promiseSanitizationDialogReady();
+
       win.addEventListener("load", function onload(event) {
         win.removeEventListener("load", onload, false);
 
         if (win.name !== "SanitizeDialog")
           return;
 
         wh.win = win;
         loaded = true;
 
-        executeSoon(function () {
-          // Some exceptions that reach here don't reach the test harness, but
-          // ok()/is() do...
-          try {
-            wh.onload();
-          }
-          catch (exc) {
-            win.close();
-            ok(false, "Unexpected exception: " + exc + "\n" + exc.stack);
-            finish();
-          }
+        Task.spawn(function*() {
+          yield promiseDialogReady;
+          yield new Promise(resolve => setTimeout(resolve, 0));
+          yield wh.onload();
         });
       }, false);
 
       win.addEventListener("unload", function onunload(event) {
         if (win.name !== "SanitizeDialog") {
           win.removeEventListener("unload", onunload, false);
           return;
         }
 
         // Why is unload fired before load?
         if (!loaded)
           return;
 
         win.removeEventListener("unload", onunload, false);
         wh.win = win;
 
-        executeSoon(function () {
-          // Some exceptions that reach here don't reach the test harness, but
-          // ok()/is() do...
-          try {
-            if (wh.onunload) {
-              Task.spawn(wh.onunload).then(function() {
-                waitForAsyncUpdates(doNextTest);
-              }).then(null, Components.utils.reportError);
-            } else {
-              waitForAsyncUpdates(doNextTest);
-            }
+        // Some exceptions that reach here don't reach the test harness, but
+        // ok()/is() do...
+        Task.spawn(function*() {
+          if (wh.onunload) {
+            yield wh.onunload();
           }
-          catch (exc) {
-            win.close();
-            ok(false, "Unexpected exception: " + exc + "\n" + exc.stack);
-            finish();
-          }
+          yield PlacesTestUtils.promiseAsyncUpdates();
+          wh._resolveClosed();
         });
       }, false);
     }
     Services.ww.registerNotification(windowObserver);
     Services.ww.openWindow(null,
                            "chrome://browser/content/sanitize.xul",
                            "SanitizeDialog",
                            "chrome,titlebar,dialog,centerscreen,modal",
@@ -904,16 +905,24 @@ WindowHelper.prototype = {
   /**
    * Toggles the details progressive disclosure button.
    */
   toggleDetails: function () {
     this.getDetailsButton().click();
   }
 };
 
+function promiseSanitizationDialogReady() {
+  return promiseTopicObserved("sanitize-dialog-setup-complete");
+}
+
+function promiseSanitizationComplete() {
+  return promiseTopicObserved("sanitizer-sanitization-complete");
+}
+
 /**
  * Adds a download to history.
  *
  * @param aMinutesAgo
  *        The download will be downloaded this many minutes ago
  */
 function addDownloadWithMinutesAgo(aExpectedPathList, aMinutesAgo) {
   let publicList = yield Downloads.getList(Downloads.PUBLIC);
@@ -935,29 +944,33 @@ function addDownloadWithMinutesAgo(aExpe
 }
 
 /**
  * Adds a form entry to history.
  *
  * @param aMinutesAgo
  *        The entry will be added this many minutes ago
  */
-function addFormEntryWithMinutesAgo(then, aMinutesAgo) {
+function promiseAddFormEntryWithMinutesAgo(aMinutesAgo) {
   let name = aMinutesAgo + "-minutes-ago";
 
   // Artifically age the entry to the proper vintage.
   let timestamp = now_uSec - (aMinutesAgo * kUsecPerMin);
 
-  FormHistory.update({ op: "add", fieldname: name, value: "dummy", firstUsed: timestamp },
+  return new Promise((resolve, reject) =>
+    FormHistory.update({ op: "add", fieldname: name, value: "dummy", firstUsed: timestamp },
                      { handleError: function (error) {
                          do_throw("Error occurred updating form history: " + error);
+                         reject();
                        },
-                       handleCompletion: function (reason) { then.next(); }
-                     });
-  return name;
+                       handleCompletion: function (reason) {
+                         resolve(name);
+                       }
+                     })
+   )
 }
 
 /**
  * Checks if a form entry exists.
  */
 function formNameExists(name)
 {
   let deferred = Promise.defer();
@@ -1035,32 +1048,16 @@ function downloadExists(aPath)
   return Task.spawn(function() {
     let publicList = yield Downloads.getList(Downloads.PUBLIC);
     let listArray = yield publicList.getAll();
     throw new Task.Result(listArray.some(i => i.target.path == aPath));
   });
 }
 
 /**
- * Runs the next test in the gAllTests array.  If all tests have been run,
- * finishes the entire suite.
- */
-function doNextTest() {
-  if (gAllTests.length <= gCurrTest) {
-    blankSlate();
-    waitForAsyncUpdates(finish);
-  }
-  else {
-    let ct = gCurrTest;
-    gCurrTest++;
-    gAllTests[ct]();
-  }
-}
-
-/**
  * Ensures that the specified downloads are either cleared or not.
  *
  * @param aDownloadIDs
  *        Array of download database IDs
  * @param aShouldBeCleared
  *        True if each download should be cleared, false otherwise
  */
 function ensureDownloadsClearedState(aDownloadIDs, aShouldBeCleared) {
@@ -1089,18 +1086,8 @@ function intPrefIs(aPrefName, aExpectedV
  * Creates a visit time.
  *
  * @param aMinutesAgo
  *        The visit will be visited this many minutes ago
  */
 function visitTimeForMinutesAgo(aMinutesAgo) {
   return now_uSec - aMinutesAgo * kUsecPerMin;
 }
-
-///////////////////////////////////////////////////////////////////////////////
-
-function test() {
-  requestLongerTimeout(2);
-  waitForExplicitFinish();
-  blankSlate();
-  // Kick off all the tests in the gAllTests array.
-  waitForAsyncUpdates(doNextTest);
-}
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -133,17 +133,17 @@ browser.jar:
         content/browser/sync/utils.js                 (content/sync/utils.js)
 *       content/browser/sync/customize.xul            (content/sync/customize.xul)
         content/browser/sync/customize.js             (content/sync/customize.js)
         content/browser/sync/customize.css            (content/sync/customize.css)
 #endif
         content/browser/safeMode.css                  (content/safeMode.css)
         content/browser/safeMode.js                   (content/safeMode.js)
         content/browser/safeMode.xul                  (content/safeMode.xul)
-*       content/browser/sanitize.js                   (content/sanitize.js)
+        content/browser/sanitize.js                   (content/sanitize.js)
 *       content/browser/sanitize.xul                  (content/sanitize.xul)
 *       content/browser/sanitizeDialog.js             (content/sanitizeDialog.js)
         content/browser/sanitizeDialog.css            (content/sanitizeDialog.css)
         content/browser/contentSearchUI.js            (content/contentSearchUI.js)
         content/browser/contentSearchUI.css           (content/contentSearchUI.css)
         content/browser/tabbrowser.css                (content/tabbrowser.css)
         content/browser/tabbrowser.xml                (content/tabbrowser.xml)
         content/browser/urlbarBindings.xml            (content/urlbarBindings.xml)
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -511,16 +511,19 @@ BrowserGlue.prototype = {
         this._handleURLBarTelemetry(subject.QueryInterface(Ci.nsIAutoCompleteInput));
         break;
       case "tablet-mode-change":
         if (data == "tablet-mode") {
           Services.telemetry.getHistogramById("FX_TABLET_MODE_USED_DURING_SESSION")
                             .add(1);
         }
         break;
+      case "test-initialize-sanitizer":
+        this._sanitizer.onStartup();
+        break;
     }
   },
 
   _handleURLBarTelemetry(input) {
     if (!input ||
         input.id != "urlbar" ||
         input.inPrivateContext ||
         input.popup.selectedIndex < 0) {
@@ -1795,17 +1798,16 @@ BrowserGlue.prototype = {
   },
 
   /**
    * Places shut-down tasks
    * - finalize components depending on Places.
    * - export bookmarks as HTML, if so configured.
    */
   _onPlacesShutdown: function BG__onPlacesShutdown() {
-    this._sanitizer.onShutdown();
     PageThumbs.uninit();
 
     if (this._bookmarksBackupIdleTime) {
       this._idleService.removeIdleObserver(this, this._bookmarksBackupIdleTime);
       delete this._bookmarksBackupIdleTime;
     }
   },
 
--- a/browser/components/places/tests/unit/test_clearHistory_shutdown.js
+++ b/browser/components/places/tests/unit/test_clearHistory_shutdown.js
@@ -44,16 +44,18 @@ function run_test() {
 
 add_task(function* test_execute() {
   do_print("Initialize browserglue before Places");
 
   // Avoid default bookmarks import.
   let glue = Cc["@mozilla.org/browser/browserglue;1"].
              getService(Ci.nsIObserver);
   glue.observe(null, "initial-migration-will-import-default-bookmarks", null);
+  glue.observe(null, "test-initialize-sanitizer", null);
+
 
   Services.prefs.setBoolPref("privacy.clearOnShutdown.cache", true);
   Services.prefs.setBoolPref("privacy.clearOnShutdown.cookies", true);
   Services.prefs.setBoolPref("privacy.clearOnShutdown.offlineApps", true);
   Services.prefs.setBoolPref("privacy.clearOnShutdown.history", true);
   Services.prefs.setBoolPref("privacy.clearOnShutdown.downloads", true);
   Services.prefs.setBoolPref("privacy.clearOnShutdown.cookies", true);
   Services.prefs.setBoolPref("privacy.clearOnShutdown.formData", true);
new file mode 100644
--- /dev/null
+++ b/browser/modules/Sanitizer.jsm
@@ -0,0 +1,22 @@
+/* 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/. */
+"use strict";
+
+//
+// A shared module for sanitize.js
+//
+// Until bug 1167238 lands, this serves only as a way to ensure that
+// sanitize is loaded from its own compartment, rather than from that
+// of the sanitize dialog.
+//
+
+this.EXPORTED_SYMBOLS = ["Sanitizer"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+let scope = {};
+Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader)
+ .loadSubScript("chrome://browser/content/sanitize.js", scope);
+
+this.Sanitizer = scope.Sanitizer;
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -33,16 +33,17 @@ EXTRA_JS_MODULES += [
     'NewTabURL.jsm',
     'offlineAppCache.jsm',
     'PanelFrame.jsm',
     'PluginContent.jsm',
     'ProcessHangMonitor.jsm',
     'ReaderParent.jsm',
     'RecentWindow.jsm',
     'RemotePrompt.jsm',
+    'Sanitizer.jsm',
     'SelfSupportBackend.jsm',
     'SitePermissions.jsm',
     'Social.jsm',
     'TransientPrefs.jsm',
     'WebappManager.jsm',
     'webrtcUI.jsm',
 ]