Merge fx-team to m-c.
authorRyan VanderMeulen <ryanvm@gmail.com>
Sat, 27 Jul 2013 21:51:16 -0400
changeset 140264 015135250e0635af3b4ef2d65f2391c6476a3833
parent 140263 ccf23dae106680452db82a8bf855865cc170e394 (current diff)
parent 140219 ad20fa11511883f4db8cd7ad585dc3979a700a5a (diff)
child 140267 73b69c146ca6926b4a72bb484550e5afe04b93cc
push id1951
push userryanvm@gmail.com
push dateSun, 28 Jul 2013 01:55:51 +0000
treeherderfx-team@73b69c146ca6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone25.0a1
Merge fx-team to m-c.
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -132,16 +132,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource:///modules/BrowserNewTabPreloader.jsm", "BrowserNewTabPreloader");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "SitePermissions",
   "resource:///modules/SitePermissions.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
+  "resource:///modules/sessionstore/SessionStore.jsm");
+
 let gInitialPages = [
   "about:blank",
   "about:newtab",
   "about:home",
   "about:privatebrowsing",
   "about:welcomeback",
   "about:sessionrestore"
 ];
@@ -1028,16 +1031,17 @@ var gBrowserInit = {
     Services.obs.addObserver(gXPInstallObserver, "addon-install-failed", false);
     Services.obs.addObserver(gXPInstallObserver, "addon-install-complete", false);
     Services.obs.addObserver(gFormSubmitObserver, "invalidformsubmit", false);
 
     BrowserOffline.init();
     OfflineApps.init();
     IndexedDBPromptHelper.init();
     gFormSubmitObserver.init();
+    SocialUI.init();
     AddonManager.addAddonListener(AddonsMgrListener);
     WebrtcIndicator.init();
 
     // Ensure login manager is up and running.
     Services.logins;
 
     if (mustLoadSidebar) {
       let sidebar = document.getElementById("sidebar");
@@ -1086,20 +1090,16 @@ var gBrowserInit = {
 
     // Bug 666804 - NetworkPrioritizer support for e10s
     if (!gMultiProcessBrowser) {
       let NP = {};
       Cu.import("resource:///modules/NetworkPrioritizer.jsm", NP);
       NP.trackBrowserWindow(window);
     }
 
-    // initialize the session-restore service (in case it's not already running)
-    let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
-    let ssPromise = ss.init(window);
-
     PlacesToolbarHelper.init();
 
     ctrlTab.readPref();
     gPrefService.addObserver(ctrlTab.prefName, ctrlTab, false);
 
     // Initialize the download manager some time after the app starts so that
     // auto-resume downloads begin (such as after crashing or quitting with
     // active downloads) and speeds up the first-load of the download manager UI.
@@ -1265,24 +1265,23 @@ var gBrowserInit = {
 #ifdef MOZ_METRO
     gMetroPrefs.prefDomain.forEach(function(prefName) {
       gMetroPrefs.pushDesktopControlledPrefToMetro(prefName);
       Services.prefs.addObserver(prefName, gMetroPrefs, false);
     }, this);
 #endif
 #endif
 
-    ssPromise.then(() =>{
+    SessionStore.promiseInitialized.then(() => {
       // Enable the Restore Last Session command if needed
-      if (ss.canRestoreLastSession &&
+      if (SessionStore.canRestoreLastSession &&
           !PrivateBrowsingUtils.isWindowPrivate(window))
         goSetCommandEnabled("Browser:RestoreLastSession", true);
 
       TabView.init();
-      SocialUI.init();
 
       setTimeout(function () { BrowserChromeTest.markAsReady(); }, 0);
     });
 
     Services.obs.notifyObservers(window, "browser-delayed-startup-finished", "");
     TelemetryTimestamps.add("delayedStartupFinished");
   },
 
--- a/browser/base/content/test/social/browser_social_window.js
+++ b/browser/base/content/test/social/browser_social_window.js
@@ -12,32 +12,26 @@ function resetSocial() {
   Social._provider = null;
   Social.providers = [];
   // *sob* - listeners keep getting added...
   let SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
   SocialService._providerListeners.clear();
 }
 
 let createdWindows = [];
-let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
 
 function openWindowAndWaitForInit(callback) {
   // this notification tells us SocialUI.init() has been run...
   let topic = "browser-delayed-startup-finished";
   let w = OpenBrowserWindow();
   createdWindows.push(w);
   Services.obs.addObserver(function providerSet(subject, topic, data) {
     Services.obs.removeObserver(providerSet, topic);
     info(topic + " observer was notified - continuing test");
-    // We need to wait for the SessionStore as well, since
-    // SocialUI.init() is also waiting on it.
-    ss.init(w).then(function () {
-      executeSoon(function() {callback(w);});
-    });
-
+    executeSoon(() => callback(w));
   }, topic, false);
 }
 
 function postTestCleanup(cb) {
   for (let w of createdWindows)
     w.close();
   createdWindows = [];
   Services.prefs.clearUserPref("social.enabled");
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -63,16 +63,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/Task.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
                                   "resource://gre/modules/PlacesBackups.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
+                                  "resource:///modules/sessionstore/SessionStore.jsm");
+
 const PREF_PLUGINS_NOTIFYUSER = "plugins.update.notifyUser";
 const PREF_PLUGINS_UPDATEURL  = "plugins.update.url";
 
 // We try to backup bookmarks at idle times, to avoid doing that at shutdown.
 // Number of idle seconds before trying to backup bookmarks.  15 minutes.
 const BOOKMARKS_BACKUP_IDLE_TIME = 15 * 60;
 // Minimum interval in milliseconds between backups.
 const BOOKMARKS_BACKUP_INTERVAL = 86400 * 1000;
@@ -168,17 +171,17 @@ BrowserGlue.prototype = {
     switch (topic) {
       case "prefservice:after-app-defaults":
         this._onAppDefaults();
         break;
       case "final-ui-startup":
         this._finalUIStartup();
         break;
       case "browser-delayed-startup-finished":
-        this._onFirstWindowLoaded();
+        this._onFirstWindowLoaded(subject);
         Services.obs.removeObserver(this, "browser-delayed-startup-finished");
         break;
       case "sessionstore-windows-restored":
         this._onWindowsRestored();
         break;
       case "browser:purge-session-history":
         // reset the console service's error buffer
         Services.console.logStringMessage(null); // clear the console (in case it's open)
@@ -574,36 +577,36 @@ BrowserGlue.prototype = {
 
     let nb = win.document.getElementById("global-notificationbox");
     nb.appendNotification(message, "reset-unused-profile",
                           "chrome://global/skin/icons/question-16.png",
                           nb.PRIORITY_INFO_LOW, buttons);
   },
 
   // the first browser window has finished initializing
-  _onFirstWindowLoaded: function BG__onFirstWindowLoaded() {
+  _onFirstWindowLoaded: function BG__onFirstWindowLoaded(aWindow) {
 #ifdef XP_WIN
     // For windows seven, initialize the jump list module.
     const WINTASKBAR_CONTRACTID = "@mozilla.org/windows-taskbar;1";
     if (WINTASKBAR_CONTRACTID in Cc &&
         Cc[WINTASKBAR_CONTRACTID].getService(Ci.nsIWinTaskbar).available) {
       let temp = {};
       Cu.import("resource:///modules/WindowsJumpLists.jsm", temp);
       temp.WinTaskbarJumpList.startup();
     }
 #endif
 
+    SessionStore.init(aWindow);
     this._trackSlowStartup();
 
     // Offer to reset a user's profile if it hasn't been used for 60 days.
     const OFFER_PROFILE_RESET_INTERVAL_MS = 60 * 24 * 60 * 60 * 1000;
-    let processStartupTime = Services.startup.getStartupInfo().process;
     let lastUse = Services.appinfo.replacedLockTime;
-    if (processStartupTime && lastUse &&
-        processStartupTime.getTime() - lastUse >= OFFER_PROFILE_RESET_INTERVAL_MS) {
+    if (lastUse &&
+        Date.now() - lastUse >= OFFER_PROFILE_RESET_INTERVAL_MS) {
       this._resetUnusedProfileNotification();
     }
   },
 
   /**
    * Profile shutdown handler (contains profile cleanup routines).
    * All components depending on Places should be shut down in
    * _onPlacesShutdown() and not here.
--- a/browser/components/sessionstore/content/content-sessionStore.js
+++ b/browser/components/sessionstore/content/content-sessionStore.js
@@ -1,40 +1,77 @@
 /* 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/. */
 
 function debug(msg) {
   Services.console.logStringMessage("SessionStoreContent: " + msg);
 }
 
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+
 /**
  * Listens for and handles content events that we need for the
  * session store service to be notified of state changes in content.
  */
 let EventListener = {
 
   DOM_EVENTS: [
-    "pageshow", "change", "input"
+    "pageshow", "change", "input", "MozStorageChanged"
   ],
 
   init: function () {
     this.DOM_EVENTS.forEach(e => addEventListener(e, this, true));
   },
 
   handleEvent: function (event) {
     switch (event.type) {
       case "pageshow":
         if (event.persisted)
           sendAsyncMessage("SessionStore:pageshow");
         break;
       case "input":
       case "change":
         sendAsyncMessage("SessionStore:input");
         break;
+      case "MozStorageChanged": {
+        let isSessionStorage = true;
+        // We are only interested in sessionStorage events
+        try {
+          if (event.storageArea != content.sessionStorage) {
+            isSessionStorage = false;
+          }
+        } catch (ex) {
+          // This page does not even have sessionStorage
+          // (this is typically the case of about: pages)
+          isSessionStorage = false;
+        }
+        if (isSessionStorage) {
+          sendAsyncMessage("SessionStore:MozStorageChanged");
+        }
+        break;
+      }
       default:
         debug("received unknown event '" + event.type + "'");
         break;
     }
   }
 };
+EventListener.init();
 
-EventListener.init();
+let ProgressListener = {
+  init: function() {
+    let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                              .getInterface(Ci.nsIWebProgress);
+    webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
+  },
+  onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) {
+    // We are changing page, so time to invalidate the state of the tab
+    sendAsyncMessage("SessionStore:loadStart");
+  },
+  onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {},
+  onProgressChange: function() {},
+  onStatusChange: function() {},
+  onSecurityChange: function() {},
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+                                         Ci.nsISupportsWeakReference])
+};
+ProgressListener.init();
--- a/browser/components/sessionstore/nsISessionStore.idl
+++ b/browser/components/sessionstore/nsISessionStore.idl
@@ -20,25 +20,20 @@ interface nsIDOMNode;
  * global |window| object to the API, though (or |top| from a sidebar).
  * From elsewhere you can get browser windows through the nsIWindowMediator
  * by looking for "navigator:browser" windows.
  *
  * * "Tabbrowser tabs" are all the child nodes of a browser window's
  * |gBrowser.tabContainer| such as e.g. |gBrowser.selectedTab|.
  */
 
-[scriptable, uuid(092fa0cc-e99b-11e2-a2a3-a25b4f45d8e2)]
+[scriptable, uuid(700756cc-f5c7-11e2-b842-59d9dc830245)]
 interface nsISessionStore : nsISupports
 {
   /**
-   * Initialize the service
-   */
-  jsval init(in nsIDOMWindow aWindow);
-
-  /**
    * Is it possible to restore the previous session. Will always be false when
    * in Private Browsing mode.
    */
   attribute boolean canRestoreLastSession;
 
   /**
    * Restore the previous session if possible. This will not overwrite the
    * current session. Instead the previous session will be merged into the
--- a/browser/components/sessionstore/src/SessionStore.jsm
+++ b/browser/components/sessionstore/src/SessionStore.jsm
@@ -53,17 +53,25 @@ const MESSAGES = [
   // The content script tells us that its form data (or that of one of its
   // subframes) might have changed. This can be the contents or values of
   // standard form fields or of ContentEditables.
   "SessionStore:input",
 
   // The content script has received a pageshow event. This happens when a
   // page is loaded from bfcache without any network activity, i.e. when
   // clicking the back or forward button.
-  "SessionStore:pageshow"
+  "SessionStore:pageshow",
+
+  // The content script has received a MozStorageChanged event dealing
+  // with a change in the contents of the sessionStorage.
+  "SessionStore:MozStorageChanged",
+
+  // The content script tells us that a new page just started loading in a
+  // browser.
+  "SessionStore:loadStart"
 ];
 
 // These are tab events that we listen to.
 const TAB_EVENTS = [
   "TabOpen", "TabClose", "TabSelect", "TabShow", "TabHide", "TabPinned",
   "TabUnpinned"
 ];
 
@@ -115,36 +123,43 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "_SessionFile",
   "resource:///modules/sessionstore/_SessionFile.jsm");
 
 #ifdef MOZ_CRASHREPORTER
 XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter",
   "@mozilla.org/xre/app-info;1", "nsICrashReporter");
 #endif
 
+/**
+ * |true| if we are in debug mode, |false| otherwise.
+ * Debug mode is controlled by preference browser.sessionstore.debug
+ */
+let gDebuggingEnabled = false;
 function debug(aMsg) {
-  aMsg = ("SessionStore: " + aMsg).replace(/\S{80}/g, "$&\n");
-  Services.console.logStringMessage(aMsg);
+  if (gDebuggingEnabled) {
+    aMsg = ("SessionStore: " + aMsg).replace(/\S{80}/g, "$&\n");
+    Services.console.logStringMessage(aMsg);
+  }
 }
 
 this.SessionStore = {
   get promiseInitialized() {
-    return SessionStoreInternal.promiseInitialized.promise;
+    return SessionStoreInternal.promiseInitialized;
   },
 
   get canRestoreLastSession() {
     return SessionStoreInternal.canRestoreLastSession;
   },
 
   set canRestoreLastSession(val) {
     SessionStoreInternal.canRestoreLastSession = val;
   },
 
   init: function ss_init(aWindow) {
-    return SessionStoreInternal.init(aWindow);
+    SessionStoreInternal.init(aWindow);
   },
 
   getBrowserState: function ss_getBrowserState() {
     return SessionStoreInternal.getBrowserState();
   },
 
   setBrowserState: function ss_setBrowserState(aState) {
     SessionStoreInternal.setBrowserState(aState);
@@ -320,17 +335,17 @@ let SessionStoreInternal = {
   _lastSessionState: null,
 
   // When starting Firefox with a single private window, this is the place
   // where we keep the session we actually wanted to restore in case the user
   // decides to later open a non-private window as well.
   _deferredInitialState: null,
 
   // A promise resolved once initialization is complete
-  _promiseInitialization: Promise.defer(),
+  _deferredInitialized: Promise.defer(),
 
   // Whether session has been initialized
   _sessionInitialized: false,
 
   // True if session store is disabled by multi-process browsing.
   // See bug 516755.
   _disabledForMultiProcess: false,
 
@@ -343,56 +358,69 @@ let SessionStoreInternal = {
   // previous session is not always restored when
   // "sessionstore.resume_from_crash" is true.
   _resume_session_once_on_shutdown: null,
 
   /**
    * A promise fulfilled once initialization is complete.
    */
   get promiseInitialized() {
-    return this._promiseInitialization;
+    return this._deferredInitialized.promise;
   },
 
   /* ........ Public Getters .............. */
   get canRestoreLastSession() {
     return this._lastSessionState;
   },
 
   set canRestoreLastSession(val) {
     // Cheat a bit; only allow false.
     if (val)
       return;
     this._lastSessionState = null;
   },
 
-  /* ........ Global Event Handlers .............. */
-
   /**
-   * Initialize the component
+   * Initialize the sessionstore service.
    */
-  initService: function ssi_initService() {
-    if (this._sessionInitialized) {
-      return;
-    }
+  init: function (aWindow) {
+    if (this._initialized) {
+      throw new Error("SessionStore.init() must only be called once!");
+    }
+
+    if (!aWindow) {
+      throw new Error("SessionStore.init() must be called with a valid window.");
+    }
+
     TelemetryTimestamps.add("sessionRestoreInitialized");
     OBSERVING.forEach(function(aTopic) {
       Services.obs.addObserver(this, aTopic, true);
     }, this);
 
     this._initPrefs();
-
+    this._initialized = true;
     this._disabledForMultiProcess = this._prefBranch.getBoolPref("tabs.remote");
 
     // this pref is only read at startup, so no need to observe it
     this._sessionhistory_max_entries =
       this._prefBranch.getIntPref("sessionhistory.max_entries");
 
-    gSessionStartup.onceInitialized.then(
-      this.initSession.bind(this)
-    );
+    // Wait until nsISessionStartup has finished reading the session data.
+    gSessionStartup.onceInitialized.then(() => {
+      // Parse session data and start restoring.
+      this.initSession();
+
+      // Start tracking the given (initial) browser window.
+      if (!aWindow.closed) {
+        this.onLoad(aWindow);
+      }
+
+      // Let everyone know we're done.
+      this._deferredInitialized.resolve();
+    });
   },
 
   initSession: function ssi_initSession() {
     let ss = gSessionStartup;
     try {
       if (ss.doRestore() ||
           ss.sessionType == Ci.nsISessionStartup.DEFER_SESSION)
         this._initialState = ss.state;
@@ -461,48 +489,27 @@ let SessionStoreInternal = {
           this._initialState.windows.forEach(function(aWindow) {
             delete aWindow.__lastSessionWindowID;
           });
         }
       }
       catch (ex) { debug("The session file is invalid: " + ex); }
     }
 
-    // A Lazy getter for the sessionstore.js backup promise.
-    XPCOMUtils.defineLazyGetter(this, "_backupSessionFileOnce", function () {
-      // We're creating a backup of sessionstore.js by moving it to .bak
-      // because that's a lot faster than creating a copy. sessionstore.js
-      // would be overwritten shortly afterwards anyway so we can save time
-      // and just move instead of copy.
-      return _SessionFile.moveToBackupPath();
-    });
-
     // at this point, we've as good as resumed the session, so we can
     // clear the resume_session_once flag, if it's set
     if (this._loadState != STATE_QUITTING &&
         this._prefBranch.getBoolPref("sessionstore.resume_session_once"))
       this._prefBranch.setBoolPref("sessionstore.resume_session_once", false);
 
     this._initEncoding();
 
     this._performUpgradeBackup();
 
-    // The service is ready. Backup-on-upgrade might still be in progress,
-    // but we do not have a race condition:
-    //
-    // - if the file to backup is named sessionstore.js, secondary
-    // backup will be started in this tick, so any further I/O will be
-    // scheduled to start after the secondary backup is complete;
-    //
-    // - if the file is named sessionstore.bak, it will only be erased
-    // by the getter to |_backupSessionFileOnce|, which specifically
-    // waits until the secondary backup has been completed or deemed
-    // useless before causing any side-effects.
     this._sessionInitialized = true;
-    this._promiseInitialization.resolve();
   },
 
   /**
    * If this is the first time we launc this build of Firefox,
    * backup sessionstore.js.
    */
   _performUpgradeBackup: function ssi_performUpgradeBackup() {
     // Perform upgrade backup, if necessary
@@ -532,19 +539,23 @@ let SessionStoreInternal = {
   _initEncoding : function ssi_initEncoding() {
     // The (UTF-8) encoder used to write to files.
     XPCOMUtils.defineLazyGetter(this, "_writeFileEncoder", function () {
       return new TextEncoder();
     });
   },
 
   _initPrefs : function() {
-    XPCOMUtils.defineLazyGetter(this, "_prefBranch", function () {
-      return Services.prefs.getBranch("browser.");
-    });
+    this._prefBranch = Services.prefs.getBranch("browser.");
+
+    gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug");
+
+    Services.prefs.addObserver("browser.sessionstore.debug", () => {
+      gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug");
+    }, false);
 
     // minimal interval between two save operations (in milliseconds)
     XPCOMUtils.defineLazyGetter(this, "_interval", function () {
       // used often, so caching/observing instead of fetching on-demand
       this._prefBranch.addObserver("sessionstore.interval", this, true);
       return this._prefBranch.getIntPref("sessionstore.interval");
     });
 
@@ -562,45 +573,28 @@ let SessionStoreInternal = {
 
     XPCOMUtils.defineLazyGetter(this, "_max_windows_undo", function () {
       this._prefBranch.addObserver("sessionstore.max_windows_undo", this, true);
       return this._prefBranch.getIntPref("sessionstore.max_windows_undo");
     });
   },
 
   /**
-   * Start tracking a window.
-   *
-   * This function also initializes the component if it is not
-   * initialized yet.
-   */
-  init: function ssi_init(aWindow) {
-    if (!aWindow) {
-      throw new Error("init() must be called with a valid window.");
-    }
-
-    let self = this;
-    this.initService();
-    return this._promiseInitialization.promise.then(
-      function onSuccess() {
-        if (!aWindow.closed) {
-          self.onLoad(aWindow);
-        }
-      }
-    );
-  },
-
-  /**
    * Called on application shutdown, after notifications:
    * quit-application-granted, quit-application
    */
   _uninit: function ssi_uninit() {
+    if (!this._initialized) {
+      throw new Error("SessionStore is not initialized.");
+    }
+
     // save all data for session resuming
-    if (this._sessionInitialized)
+    if (this._sessionInitialized) {
       this.saveState(true);
+    }
 
     // clear out priority queue in case it's still holding refs
     TabRestoreQueue.reset();
 
     // Make sure to break our cycle with the save timer
     if (this._saveTimer) {
       this._saveTimer.cancel();
       this._saveTimer = null;
@@ -658,16 +652,23 @@ let SessionStoreInternal = {
 
     switch (aMessage.name) {
       case "SessionStore:pageshow":
         this.onTabLoad(win, browser);
         break;
       case "SessionStore:input":
         this.onTabInput(win, browser);
         break;
+      case "SessionStore:MozStorageChanged":
+        TabStateCache.delete(browser);
+        this.saveStateDelayed(win);
+        break;
+      case "SessionStore:loadStart":
+        TabStateCache.delete(browser);
+        break;
       default:
         debug("received unknown message '" + aMessage.name + "'");
         break;
     }
 
     this._clearRestoringWindows();
   },
 
@@ -682,16 +683,17 @@ let SessionStoreInternal = {
 
     var win = aEvent.currentTarget.ownerDocument.defaultView;
     switch (aEvent.type) {
       case "load":
         // If __SS_restore_data is set, then we need to restore the document
         // (form data, scrolling, etc.). This will only happen when a tab is
         // first restored.
         let browser = aEvent.currentTarget;
+        TabStateCache.delete(browser);
         if (browser.__SS_restore_data)
           this.restoreDocument(win, browser, aEvent);
         this.onTabLoad(win, browser);
         break;
       case "TabOpen":
         this.onTabAdd(win, aEvent.originalTarget);
         break;
       case "TabClose":
@@ -705,21 +707,26 @@ let SessionStoreInternal = {
         break;
       case "TabShow":
         this.onTabShow(win, aEvent.originalTarget);
         break;
       case "TabHide":
         this.onTabHide(win, aEvent.originalTarget);
         break;
       case "TabPinned":
-      case "TabUnpinned":
+        // If possible, update cached data without having to invalidate it
+        TabStateCache.update(aEvent.originalTarget, "pinned", true);
         this.saveStateDelayed(win);
         break;
-    }
-
+      case "TabUnpinned":
+        // If possible, update cached data without having to invalidate it
+        TabStateCache.update(aEvent.originalTarget, "pinned", false);
+        this.saveStateDelayed(win);
+        break;
+    }
     this._clearRestoringWindows();
   },
 
   /**
    * If it's the first window load since app start...
    * - determine if we're reloading after a crash or a forced-restart
    * - restore window state
    * - restart downloads
@@ -1079,16 +1086,17 @@ let SessionStoreInternal = {
     // session data on disk as this notification fires after the
     // quit-application notification so the browser is about to exit.
     if (this._loadState == STATE_QUITTING)
       return;
     this._lastSessionState = null;
     let openWindows = {};
     this._forEachBrowserWindow(function(aWindow) {
       Array.forEach(aWindow.gBrowser.tabs, function(aTab) {
+        TabStateCache.delete(aTab);
         delete aTab.linkedBrowser.__SS_data;
         delete aTab.linkedBrowser.__SS_tabStillLoading;
         delete aTab.linkedBrowser.__SS_formDataSaved;
         delete aTab.linkedBrowser.__SS_hostSchemeData;
         if (aTab.linkedBrowser.__SS_restoreState)
           this._resetTabRestoringState(aTab);
       });
       openWindows[aWindow.__SSi] = true;
@@ -1302,19 +1310,18 @@ let SessionStoreInternal = {
     event.initEvent("SSTabClosing", true, false);
     aTab.dispatchEvent(event);
 
     // don't update our internal state if we don't have to
     if (this._max_tabs_undo == 0) {
       return;
     }
 
-    // make sure that the tab related data is up-to-date
-    var tabState = this._collectTabData(aTab);
-    this._updateTextAndScrollDataForTab(aWindow, aTab.linkedBrowser, tabState);
+    // Get the latest data for this tab (generally, from the cache)
+    let tabState = this._collectTabData(aTab);
 
     // store closed-tab data for undo
     if (this._shouldSaveTabState(tabState)) {
       let tabTitle = aTab.label;
       let tabbrowser = aWindow.gBrowser;
       tabTitle = this._replaceLoadingTitle(tabTitle, tabbrowser, aTab);
 
       this._windows[aWindow.__SSi]._closedTabs.unshift({
@@ -1325,32 +1332,35 @@ let SessionStoreInternal = {
       });
       var length = this._windows[aWindow.__SSi]._closedTabs.length;
       if (length > this._max_tabs_undo)
         this._windows[aWindow.__SSi]._closedTabs.splice(this._max_tabs_undo, length - this._max_tabs_undo);
     }
   },
 
   /**
-   * When a tab loads, save state.
+   * When a tab loads, invalidate its cached state, trigger async save.
+   *
    * @param aWindow
    *        Window reference
    * @param aBrowser
    *        Browser reference
    */
   onTabLoad: function ssi_onTabLoad(aWindow, aBrowser) {
     // react on "load" and solitary "pageshow" events (the first "pageshow"
     // following "load" is too late for deleting the data caches)
     // It's possible to get a load event after calling stop on a browser (when
     // overwriting tabs). We want to return early if the tab hasn't been restored yet.
     if (aBrowser.__SS_restoreState &&
         aBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
       return;
     }
 
+    TabStateCache.delete(aBrowser);
+
     delete aBrowser.__SS_data;
     delete aBrowser.__SS_tabStillLoading;
     delete aBrowser.__SS_formDataSaved;
     this.saveStateDelayed(aWindow);
 
     // attempt to update the current URL we send in a crash report
     this._updateCrashReportURL(aWindow);
   },
@@ -1361,16 +1371,18 @@ let SessionStoreInternal = {
    *        Window reference
    * @param aBrowser
    *        Browser reference
    */
   onTabInput: function ssi_onTabInput(aWindow, aBrowser) {
     // deleting __SS_formDataSaved will cause us to recollect form data
     delete aBrowser.__SS_formDataSaved;
 
+    TabStateCache.delete(aBrowser);
+
     this.saveStateDelayed(aWindow, 3000);
   },
 
   /**
    * When a tab is selected, save session data
    * @param aWindow
    *        Window reference
    */
@@ -1397,28 +1409,34 @@ let SessionStoreInternal = {
         aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
       TabRestoreQueue.hiddenToVisible(aTab);
 
       // let's kick off tab restoration again to ensure this tab gets restored
       // with "restore_hidden_tabs" == false (now that it has become visible)
       this.restoreNextTab();
     }
 
+    // If possible, update cached data without having to invalidate it
+    TabStateCache.update(aTab, "hidden", false);
+
     // Default delay of 2 seconds gives enough time to catch multiple TabShow
     // events due to changing groups in Panorama.
     this.saveStateDelayed(aWindow);
   },
 
   onTabHide: function ssi_onTabHide(aWindow, aTab) {
     // If the tab hasn't been restored yet, move it into the right bucket
     if (aTab.linkedBrowser.__SS_restoreState &&
         aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
       TabRestoreQueue.visibleToHidden(aTab);
     }
 
+    // If possible, update cached data without having to invalidate it
+    TabStateCache.update(aTab, "hidden", true);
+
     // Default delay of 2 seconds gives enough time to catch multiple TabHide
     // events due to changing groups in Panorama.
     this.saveStateDelayed(aWindow);
   },
 
   /* ........ nsISessionStore API .............. */
 
   getBrowserState: function ssi_getBrowserState() {
@@ -1484,42 +1502,63 @@ let SessionStoreInternal = {
 
     this.restoreWindow(aWindow, aState, aOverwrite);
   },
 
   getTabState: function ssi_getTabState(aTab) {
     if (!aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi)
       throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
 
-    var tabState = this._collectTabData(aTab);
-
-    var window = aTab.ownerDocument.defaultView;
-    this._updateTextAndScrollDataForTab(window, aTab.linkedBrowser, tabState);
+    let tabState = this._collectTabData(aTab);
 
     return this._toJSONString(tabState);
   },
 
   setTabState: function ssi_setTabState(aTab, aState) {
-    var tabState = JSON.parse(aState);
-    if (!tabState.entries || !aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi)
+    // Remove the tab state from the cache.
+    // Note that we cannot simply replace the contents of the cache
+    // as |aState| can be an incomplete state that will be completed
+    // by |restoreHistoryPrecursor|.
+    let tabState = JSON.parse(aState);
+    if (!tabState) {
+      debug("Empty state argument");
+      throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+    }
+    if (typeof tabState != "object") {
+      debug("State argument does not represent an object");
       throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
-
-    var window = aTab.ownerDocument.defaultView;
+    }
+    if (!("entries" in tabState)) {
+      debug("State argument must contain field 'entries'");
+      throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+    }
+    if (!aTab.ownerDocument) {
+      debug("Tab argument must have an owner document");
+      throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+    }
+
+    let window = aTab.ownerDocument.defaultView;
+    if (!("__SSi" in window)) {
+      debug("Default view of ownerDocument must have a unique identifier");
+      throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+    }
+
+    TabStateCache.delete(aTab);
     this._setWindowStateBusy(window);
     this.restoreHistoryPrecursor(window, [aTab], [tabState], 0, 0, 0);
   },
 
   duplicateTab: function ssi_duplicateTab(aWindow, aTab, aDelta) {
     if (!aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi ||
         !aWindow.getBrowser)
       throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
 
-    var tabState = this._collectTabData(aTab, true);
-    var sourceWindow = aTab.ownerDocument.defaultView;
-    this._updateTextAndScrollDataForTab(sourceWindow, aTab.linkedBrowser, tabState, true);
+    // Duplicate the tab state
+    let tabState = this._cloneFullTabData(aTab);
+
     tabState.index += aDelta;
     tabState.index = Math.max(1, Math.min(tabState.index, tabState.entries.length));
     tabState.pinned = false;
 
     this._setWindowStateBusy(aWindow);
     let newTab = aTab == aWindow.gBrowser.selectedTab ?
       aWindow.gBrowser.addTab(null, {relatedToCurrent: true, ownerTab: aTab}) :
       aWindow.gBrowser.addTab();
@@ -1679,31 +1718,33 @@ let SessionStoreInternal = {
       throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
     }
   },
 
   deleteWindowValue: function ssi_deleteWindowValue(aWindow, aKey) {
     if (aWindow.__SSi && this._windows[aWindow.__SSi].extData &&
         this._windows[aWindow.__SSi].extData[aKey])
       delete this._windows[aWindow.__SSi].extData[aKey];
+    this.saveStateDelayed(aWindow);
   },
 
   getTabValue: function ssi_getTabValue(aTab, aKey) {
     let data = {};
     if (aTab.__SS_extdata) {
       data = aTab.__SS_extdata;
     }
     else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) {
       // If the tab hasn't been fully restored, get the data from the to-be-restored data
       data = aTab.linkedBrowser.__SS_data.extData;
     }
     return data[aKey] || "";
   },
 
   setTabValue: function ssi_setTabValue(aTab, aKey, aStringValue) {
+    TabStateCache.delete(aTab);
     // If the tab hasn't been restored, then set the data there, otherwise we
     // could lose newly added data.
     let saveTo;
     if (aTab.__SS_extdata) {
       saveTo = aTab.__SS_extdata;
     }
     else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) {
       saveTo = aTab.linkedBrowser.__SS_data.extData;
@@ -1712,33 +1753,36 @@ let SessionStoreInternal = {
       aTab.__SS_extdata = {};
       saveTo = aTab.__SS_extdata;
     }
     saveTo[aKey] = aStringValue;
     this.saveStateDelayed(aTab.ownerDocument.defaultView);
   },
 
   deleteTabValue: function ssi_deleteTabValue(aTab, aKey) {
+    TabStateCache.delete(aTab);
     // We want to make sure that if data is accessed early, we attempt to delete
     // that data from __SS_data as well. Otherwise we'll throw in cases where
     // data can be set or read.
     let deleteFrom;
     if (aTab.__SS_extdata) {
       deleteFrom = aTab.__SS_extdata;
     }
     else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) {
       deleteFrom = aTab.linkedBrowser.__SS_data.extData;
     }
 
     if (deleteFrom && deleteFrom[aKey])
       delete deleteFrom[aKey];
+    this.saveStateDelayed(aTab.ownerDocument.defaultView);
   },
 
   persistTabAttribute: function ssi_persistTabAttribute(aName) {
     if (TabAttributes.persist(aName)) {
+      TabStateCache.clear();
       this.saveStateDelayed();
     }
   },
 
   /**
    * Restores the session state stored in _lastSessionState. This will attempt
    * to merge data into the current session. If a window was opened at startup
    * with pinned tab(s), then the remaining data from the previous session for
@@ -1902,47 +1946,66 @@ let SessionStoreInternal = {
     }
 
     return [true, canOverwriteTabs];
   },
 
   /* ........ Saving Functionality .............. */
 
   /**
-   * Store all session data for a window
-   * @param aWindow
-   *        Window reference
+   * Collect data related to a single tab
+   *
+   * @param aTab
+   *        tabbrowser tab
+   *
+   * @returns {TabData} An object with the data for this tab.  If the
+   * tab has not been invalidated since the last call to
+   * _collectTabData(aTab), the same object is returned.
    */
-  _saveWindowHistory: function ssi_saveWindowHistory(aWindow) {
-    var tabbrowser = aWindow.gBrowser;
-    var tabs = tabbrowser.tabs;
-    var tabsData = this._windows[aWindow.__SSi].tabs = [];
-
-    for (var i = 0; i < tabs.length; i++)
-      tabsData.push(this._collectTabData(tabs[i]));
-
-    this._windows[aWindow.__SSi].selected = tabbrowser.mTabBox.selectedIndex + 1;
+  _collectTabData: function ssi_collectTabData(aTab) {
+    if (!aTab) {
+      throw new TypeError("Expecting a tab");
+    }
+    let tabData;
+    if ((tabData = TabStateCache.get(aTab))) {
+      return tabData;
+    }
+    tabData = new TabData(this._collectBaseTabData(aTab));
+    if (this._updateTextAndScrollDataForTab(aTab, tabData)) {
+      TabStateCache.set(aTab, tabData);
+    }
+    return tabData;
   },
 
   /**
-   * Collect data related to a single tab
+   * Collect data related to a single tab, including private data.
+   * Use with caution.
+   *
    * @param aTab
    *        tabbrowser tab
-   * @param aFullData
-   *        always return privacy sensitive data (use with care)
-   * @returns object
+   *
+   * @returns {object} An object with the data for this tab. This object
+   * is recomputed at every call.
    */
-  _collectTabData: function ssi_collectTabData(aTab, aFullData) {
-    var tabData = { entries: [], lastAccessed: aTab.lastAccessed };
-    var browser = aTab.linkedBrowser;
-
-    if (!browser || !browser.currentURI)
+  _cloneFullTabData: function ssi_cloneFullTabData(aTab) {
+    let options = { includePrivateData: true };
+    let tabData = this._collectBaseTabData(aTab, options);
+    this._updateTextAndScrollDataForTab(aTab, tabData, options);
+    return tabData;
+  },
+
+  _collectBaseTabData: function ssi_collectBaseTabData(aTab, aOptions = null) {
+    let includePrivateData = aOptions && aOptions.includePrivateData;
+    let tabData = {entries: [], lastAccessed: aTab.lastAccessed };
+    let browser = aTab.linkedBrowser;
+    if (!browser || !browser.currentURI) {
       // can happen when calling this function right after .addTab()
       return tabData;
-    else if (browser.__SS_data && browser.__SS_tabStillLoading) {
+    }
+    if (browser.__SS_data && browser.__SS_tabStillLoading) {
       // use the data to be restored when the tab hasn't been completely loaded
       tabData = browser.__SS_data;
       if (aTab.pinned)
         tabData.pinned = true;
       else
         delete tabData.pinned;
       tabData.hidden = aTab.hidden;
 
@@ -1962,26 +2025,26 @@ let SessionStoreInternal = {
     }
     catch (ex) { } // this could happen if we catch a tab during (de)initialization
 
     // XXXzeniko anchor navigation doesn't reset __SS_data, so we could reuse
     //           data even when we shouldn't (e.g. Back, different anchor)
     if (history && browser.__SS_data &&
         browser.__SS_data.entries[history.index] &&
         browser.__SS_data.entries[history.index].url == browser.currentURI.spec &&
-        history.index < this._sessionhistory_max_entries - 1 && !aFullData) {
+        history.index < this._sessionhistory_max_entries - 1 && !includePrivateData) {
       tabData = browser.__SS_data;
       tabData.index = history.index + 1;
     }
     else if (history && history.count > 0) {
       browser.__SS_hostSchemeData = [];
       try {
         for (var j = 0; j < history.count; j++) {
           let entry = this._serializeHistoryEntry(history.getEntryAtIndex(j, false),
-                                                  aFullData, aTab.pinned, browser.__SS_hostSchemeData);
+                                                  includePrivateData, aTab.pinned, browser.__SS_hostSchemeData);
           tabData.entries.push(entry);
         }
         // If we make it through the for loop, then we're ok and we should clear
         // any indicator of brokenness.
         delete aTab.__SS_broken_history;
       }
       catch (ex) {
         // In some cases, getEntryAtIndex will throw. This seems to be due to
@@ -1997,17 +2060,17 @@ let SessionStoreInternal = {
           NS_ASSERT(false, "SessionStore failed gathering complete history " +
                            "for the focused window/tab. See bug 669196.");
           aTab.__SS_broken_history = true;
         }
       }
       tabData.index = history.index + 1;
 
       // make sure not to cache privacy sensitive data which shouldn't get out
-      if (!aFullData)
+      if (!includePrivateData)
         browser.__SS_data = tabData;
     }
     else if (browser.currentURI.spec != "about:blank" ||
              browser.contentDocument.body.hasChildNodes()) {
       tabData.entries[0] = { url: browser.currentURI.spec };
       tabData.index = 1;
     }
 
@@ -2046,39 +2109,39 @@ let SessionStoreInternal = {
     tabData.image = tabbrowser.getIcon(aTab);
 
     if (aTab.__SS_extdata)
       tabData.extData = aTab.__SS_extdata;
     else if (tabData.extData)
       delete tabData.extData;
 
     if (history && browser.docShell instanceof Ci.nsIDocShell) {
-      let storageData = SessionStorage.serialize(browser.docShell, aFullData)
+      let storageData = SessionStorage.serialize(browser.docShell, includePrivateData)
       if (Object.keys(storageData).length)
         tabData.storage = storageData;
     }
 
     return tabData;
   },
 
   /**
    * Get an object that is a serialized representation of a History entry
    * Used for data storage
    * @param aEntry
    *        nsISHEntry instance
-   * @param aFullData
+   * @param aIncludePrivateData
    *        always return privacy sensitive data (use with care)
    * @param aIsPinned
    *        the tab is pinned and should be treated differently for privacy
    * @param aHostSchemeData
    *        an array of objects with host & scheme keys
    * @returns object
    */
   _serializeHistoryEntry:
-    function ssi_serializeHistoryEntry(aEntry, aFullData, aIsPinned, aHostSchemeData) {
+    function ssi_serializeHistoryEntry(aEntry, aIncludePrivateData, aIsPinned, aHostSchemeData) {
     var entry = { url: aEntry.URI.spec };
 
     try {
       // throwing is expensive, we know that about: pages will throw
       if (entry.url.indexOf("about:") != 0)
         aHostSchemeData.push({ host: aEntry.URI.host, scheme: aEntry.URI.scheme });
     }
     catch (ex) {
@@ -2119,26 +2182,26 @@ let SessionStoreInternal = {
 
     var x = {}, y = {};
     aEntry.getScrollPosition(x, y);
     if (x.value != 0 || y.value != 0)
       entry.scroll = x.value + "," + y.value;
 
     try {
       var prefPostdata = this._prefBranch.getIntPref("sessionstore.postdata");
-      if (aEntry.postData && (aFullData || prefPostdata &&
+      if (aEntry.postData && (aIncludePrivateData || prefPostdata &&
             this.checkPrivacyLevel(aEntry.URI.schemeIs("https"), aIsPinned))) {
         aEntry.postData.QueryInterface(Ci.nsISeekableStream).
                         seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
         var stream = Cc["@mozilla.org/binaryinputstream;1"].
                      createInstance(Ci.nsIBinaryInputStream);
         stream.setInputStream(aEntry.postData);
         var postBytes = stream.readByteArray(stream.available());
         var postdata = String.fromCharCode.apply(null, postBytes);
-        if (aFullData || prefPostdata == -1 ||
+        if (aIncludePrivateData || prefPostdata == -1 ||
             postdata.replace(/^(Content-.*\r\n)+(\r\n)*/, "").length <=
               prefPostdata) {
           // We can stop doing base64 encoding once our serialization into JSON
           // is guaranteed to handle all chars in strings, including embedded
           // nulls.
           entry.postdata_b64 = btoa(postdata);
         }
       }
@@ -2189,119 +2252,113 @@ let SessionStoreInternal = {
 
         if (child) {
           // don't try to restore framesets containing wyciwyg URLs (cf. bug 424689 and bug 450595)
           if (child.URI.schemeIs("wyciwyg")) {
             children = [];
             break;
           }
 
-          children.push(this._serializeHistoryEntry(child, aFullData,
+          children.push(this._serializeHistoryEntry(child, aIncludePrivateData,
                                                     aIsPinned, aHostSchemeData));
         }
       }
 
       if (children.length)
         entry.children = children;
     }
 
     return entry;
   },
 
   /**
-   * go through all tabs and store the current scroll positions
+   * Go through all frames and store the current scroll positions
    * and innerHTML content of WYSIWYG editors
-   * @param aWindow
-   *        Window reference
-   */
-  _updateTextAndScrollData: function ssi_updateTextAndScrollData(aWindow) {
-    var browsers = aWindow.gBrowser.browsers;
-    this._windows[aWindow.__SSi].tabs.forEach(function (tabData, i) {
-      try {
-        this._updateTextAndScrollDataForTab(aWindow, browsers[i], tabData);
-      }
-      catch (ex) { debug(ex); } // get as much data as possible, ignore failures (might succeed the next time)
-    }, this);
-  },
-
-  /**
-   * go through all frames and store the current scroll positions
-   * and innerHTML content of WYSIWYG editors
-   * @param aWindow
-   *        Window reference
-   * @param aBrowser
-   *        single browser reference
+   *
+   * @param aTab
+   *        tabbrowser tab
    * @param aTabData
    *        tabData object to add the information to
-   * @param aFullData
-   *        always return privacy sensitive data (use with care)
+   * @param options
+   *        An optional object that may contain the following field:
+   *        - includePrivateData: always return privacy sensitive data
+   *          (use with care)
+   * @return false if data should not be cached because the tab
+   *        has not been fully initialized yet.
    */
   _updateTextAndScrollDataForTab:
-    function ssi_updateTextAndScrollDataForTab(aWindow, aBrowser, aTabData, aFullData) {
+    function ssi_updateTextAndScrollDataForTab(aTab, aTabData, aOptions = null) {
+    let includePrivateData = aOptions && aOptions.includePrivateData;
+    let window = aTab.ownerDocument.defaultView;
+    let browser = aTab.linkedBrowser;
     // we shouldn't update data for incompletely initialized tabs
-    if (aBrowser.__SS_data && aBrowser.__SS_tabStillLoading)
-      return;
-
-    var tabIndex = (aTabData.index || aTabData.entries.length) - 1;
+    if (!browser.currentURI
+        || (browser.__SS_data && browser.__SS_tabStillLoading)) {
+      return false;
+    }
+
+    let tabIndex = (aTabData.index || aTabData.entries.length) - 1;
     // entry data needn't exist for tabs just initialized with an incomplete session state
-    if (!aTabData.entries[tabIndex])
-      return;
-
-    let selectedPageStyle = aBrowser.markupDocumentViewer.authorStyleDisabled ? "_nostyle" :
-                            this._getSelectedPageStyle(aBrowser.contentWindow);
+    if (!aTabData.entries[tabIndex]) {
+      return false;
+    }
+
+    let selectedPageStyle = browser.markupDocumentViewer.authorStyleDisabled ? "_nostyle" :
+                            this._getSelectedPageStyle(browser.contentWindow);
     if (selectedPageStyle)
       aTabData.pageStyle = selectedPageStyle;
     else if (aTabData.pageStyle)
       delete aTabData.pageStyle;
 
-    this._updateTextAndScrollDataForFrame(aWindow, aBrowser.contentWindow,
+    this._updateTextAndScrollDataForFrame(window, browser.contentWindow,
                                           aTabData.entries[tabIndex],
-                                          !aBrowser.__SS_formDataSaved, aFullData,
+                                          !browser.__SS_formDataSaved, includePrivateData,
                                           !!aTabData.pinned);
-    aBrowser.__SS_formDataSaved = true;
-    if (aBrowser.currentURI.spec == "about:config")
+    browser.__SS_formDataSaved = true;
+    if (browser.currentURI.spec == "about:config")
       aTabData.entries[tabIndex].formdata = {
         id: {
-          "textbox": aBrowser.contentDocument.getElementById("textbox").value
+          "textbox": browser.contentDocument.getElementById("textbox").value
         },
         xpath: {}
       };
+      return true;
   },
 
   /**
    * go through all subframes and store all form data, the current
    * scroll positions and innerHTML content of WYSIWYG editors
    * @param aWindow
    *        Window reference
    * @param aContent
    *        frame reference
    * @param aData
    *        part of a tabData object to add the information to
    * @param aUpdateFormData
    *        update all form data for this tab
-   * @param aFullData
+   * @param aIncludePrivateData
    *        always return privacy sensitive data (use with care)
    * @param aIsPinned
    *        the tab is pinned and should be treated differently for privacy
    */
   _updateTextAndScrollDataForFrame:
     function ssi_updateTextAndScrollDataForFrame(aWindow, aContent, aData,
-                                                 aUpdateFormData, aFullData, aIsPinned) {
+                                                 aUpdateFormData, aIncludePrivateData, aIsPinned) {
     for (var i = 0; i < aContent.frames.length; i++) {
       if (aData.children && aData.children[i])
         this._updateTextAndScrollDataForFrame(aWindow, aContent.frames[i],
                                               aData.children[i], aUpdateFormData,
-                                              aFullData, aIsPinned);
+                                              aIncludePrivateData, aIsPinned);
     }
     var isHTTPS = this._getURIFromString((aContent.parent || aContent).
                                          document.location.href).schemeIs("https");
     let topURL = aContent.top.document.location.href;
     let isAboutSR = topURL == "about:sessionrestore" || topURL == "about:welcomeback";
-    if (aFullData || this.checkPrivacyLevel(isHTTPS, aIsPinned) || isAboutSR) {
-      if (aFullData || aUpdateFormData) {
+    if (aIncludePrivateData || this.checkPrivacyLevel(isHTTPS, aIsPinned) || isAboutSR) {
+      if (aIncludePrivateData || aUpdateFormData) {
         let formData = DocumentUtils.getFormData(aContent.document);
 
         // We want to avoid saving data for about:sessionrestore as a string.
         // Since it's stored in the form as stringified JSON, stringifying further
         // causes an explosion of escape characters. cf. bug 467409
         if (formData && isAboutSR) {
           formData.id["sessionData"] = JSON.parse(formData.id["sessionData"]);
         }
@@ -2415,38 +2472,16 @@ let SessionStoreInternal = {
       aHosts[aHost] = aIsPinned;
     }
     else if (aScheme == "file") {
       aHosts[aHost] = true;
     }
   },
 
   /**
-   * store all hosts for a URL
-   * @param aWindow
-   *        Window reference
-   */
-  _updateCookieHosts: function ssi_updateCookieHosts(aWindow) {
-    var hosts = this._internalWindows[aWindow.__SSi].hosts = {};
-
-    // Since _updateCookiesHosts is only ever called for open windows during a
-    // session, we can call into _extractHostsForCookiesFromHostScheme directly
-    // using data that is attached to each browser.
-    for (let i = 0; i < aWindow.gBrowser.tabs.length; i++) {
-      let tab = aWindow.gBrowser.tabs[i];
-      let hostSchemeData = tab.linkedBrowser.__SS_hostSchemeData || [];
-      for (let j = 0; j < hostSchemeData.length; j++) {
-        this._extractHostsForCookiesFromHostScheme(hostSchemeData[j].host,
-                                                   hostSchemeData[j].scheme,
-                                                   hosts, true, tab.pinned);
-      }
-    }
-  },
-
-  /**
    * Serialize cookie data
    * @param aWindows
    *        JS object containing window data references
    *        { id: winData, etc. }
    */
   _updateCookies: function ssi_updateCookies(aWindows) {
     function addCookieToHash(aHash, aHost, aPath, aName, aCookie) {
       // lazily build up a 3-dimensional hash, with
@@ -2673,20 +2708,39 @@ let SessionStoreInternal = {
 
     return { windows: [winData] };
   },
 
   _collectWindowData: function ssi_collectWindowData(aWindow) {
     if (!this._isWindowLoaded(aWindow))
       return;
 
+    let tabbrowser = aWindow.gBrowser;
+    let tabs = tabbrowser.tabs;
+    let winData = this._windows[aWindow.__SSi];
+    let tabsData = winData.tabs = [];
+    let hosts = this._internalWindows[aWindow.__SSi].hosts = {};
+
     // update the internal state data for this window
-    this._saveWindowHistory(aWindow);
-    this._updateTextAndScrollData(aWindow);
-    this._updateCookieHosts(aWindow);
+    for (let tab of tabs) {
+      tabsData.push(this._collectTabData(tab));
+
+      // Since we are only ever called for open
+      // windows during a session, we can call into
+      // _extractHostsForCookiesFromHostScheme directly using data
+      // that is attached to each browser.
+      let hostSchemeData = tab.linkedBrowser.__SS_hostSchemeData || [];
+      for (let j = 0; j < hostSchemeData.length; j++) {
+        this._extractHostsForCookiesFromHostScheme(hostSchemeData[j].host,
+                                                   hostSchemeData[j].scheme,
+                                                   hosts, true, tab.pinned);
+      }
+    }
+    winData.selected = tabbrowser.mTabBox.selectedIndex + 1;
+
     this._updateWindowFeatures(aWindow);
 
     // Make sure we keep __SS_lastSessionWindowID around for cases like entering
     // or leaving PB mode.
     if (aWindow.__SS_lastSessionWindowID)
       this._windows[aWindow.__SSi].__lastSessionWindowID =
         aWindow.__SS_lastSessionWindowID;
 
@@ -2810,20 +2864,23 @@ let SessionStoreInternal = {
       winData.tabs[0].hidden = false;
       tabbrowser.showTab(tabs[0]);
     }
 
     // If overwriting tabs, we want to reset each tab's "restoring" state. Since
     // we're overwriting those tabs, they should no longer be restoring. The
     // tabs will be rebuilt and marked if they need to be restored after loading
     // state (in restoreHistoryPrecursor).
+    // We also want to invalidate any cached information on the tab state.
     if (aOverwriteTabs) {
       for (let i = 0; i < tabbrowser.tabs.length; i++) {
+        let tab = tabbrowser.tabs[i];
+        TabStateCache.delete(tab);
         if (tabbrowser.browsers[i].__SS_restoreState)
-          this._resetTabRestoringState(tabbrowser.tabs[i]);
+          this._resetTabRestoringState(tab);
       }
     }
 
     // We want to set up a counter on the window that indicates how many tabs
     // in this window are unrestored. This will be used in restoreNextTab to
     // determine if gRestoreTabsProgressListener should be removed from the window.
     // If we aren't overwriting existing tabs, then we want to add to the existing
     // count in case there are still tabs restoring.
@@ -2975,32 +3032,33 @@ let SessionStoreInternal = {
    *        Counter for number of times delaying b/c browser or history aren't ready
    * @param aRestoreImmediately
    *        Flag to indicate whether the given set of tabs aTabs should be
    *        restored/loaded immediately even if restore_on_demand = true
    */
   restoreHistoryPrecursor:
     function ssi_restoreHistoryPrecursor(aWindow, aTabs, aTabData, aSelectTab,
                                          aIx, aCount, aRestoreImmediately = false) {
+
     var tabbrowser = aWindow.gBrowser;
 
     // make sure that all browsers and their histories are available
     // - if one's not, resume this check in 100ms (repeat at most 10 times)
     for (var t = aIx; t < aTabs.length; t++) {
       try {
         if (!tabbrowser.getBrowserForTab(aTabs[t]).webNavigation.sessionHistory) {
           throw new Error();
         }
       }
       catch (ex) { // in case browser or history aren't ready yet
         if (aCount < 10) {
           var restoreHistoryFunc = function(self) {
             self.restoreHistoryPrecursor(aWindow, aTabs, aTabData, aSelectTab,
                                          aIx, aCount + 1, aRestoreImmediately);
-          }
+          };
           aWindow.setTimeout(restoreHistoryFunc, 100, this);
           return;
         }
       }
     }
 
     if (!this._isWindowLoaded(aWindow)) {
       // from now on, the data will come from the actual window
@@ -3126,17 +3184,16 @@ let SessionStoreInternal = {
       // At this point we're essentially ready for consumers to read/write data
       // via the sessionstore API so we'll send the SSWindowStateReady event.
       this._setWindowStateReady(aWindow);
       return; // no more tabs to restore
     }
 
     var tab = aTabs.shift();
     var tabData = aTabData.shift();
-
     var browser = aWindow.gBrowser.getBrowserForTab(tab);
     var history = browser.webNavigation.sessionHistory;
 
     if (history.count > 0) {
       history.PurgeHistory(history.count);
     }
     history.QueryInterface(Ci.nsISHistoryInternal);
 
@@ -3691,27 +3748,27 @@ let SessionStoreInternal = {
   /**
    * save state delayed by N ms
    * marks window as dirty (i.e. data update can't be skipped)
    * @param aWindow
    *        Window reference
    * @param aDelay
    *        Milliseconds to delay
    */
-  saveStateDelayed: function ssi_saveStateDelayed(aWindow, aDelay) {
+  saveStateDelayed: function ssi_saveStateDelayed(aWindow = null, aDelay = 2000) {
     if (aWindow) {
       this._dirtyWindows[aWindow.__SSi] = true;
     }
 
     if (!this._saveTimer) {
       // interval until the next disk operation is allowed
       var minimalDelay = this._lastSaveTime + this._interval - Date.now();
 
       // if we have to wait, set a timer, otherwise saveState directly
-      aDelay = Math.max(minimalDelay, aDelay || 2000);
+      aDelay = Math.max(minimalDelay, aDelay);
       if (aDelay > 0) {
         this._saveTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
         this._saveTimer.init(this, aDelay, Ci.nsITimer.TYPE_ONE_SHOT);
       }
       else {
         this.saveState();
       }
     }
@@ -3817,35 +3874,19 @@ let SessionStoreInternal = {
     Services.obs.notifyObservers(stateString, "sessionstore-state-write", "");
     data = stateString.data;
 
     // Don't touch the file if an observer has deleted all state data.
     if (!data) {
       return;
     }
 
-    let promise;
-    // If "sessionstore.resume_from_crash" is true, attempt to backup the
-    // session file first, before writing to it.
-    if (this._resume_from_crash) {
-      // Note that we do not have race conditions here as _SessionFile
-      // guarantees that any I/O operation is completed before proceeding to
-      // the next I/O operation.
-      // Note backup happens only once, on initial save.
-      promise = this._backupSessionFileOnce;
-    } else {
-      promise = Promise.resolve();
-    }
-
-    // Attempt to write to the session file (potentially, depending on
-    // "sessionstore.resume_from_crash" preference, after successful backup).
-    promise = promise.then(function onSuccess() {
-      // Write (atomically) to a session file, using a tmp file.
-      return _SessionFile.write(data);
-    });
+    // Write (atomically) to a session file, using a tmp file.
+    let promise =
+      _SessionFile.write(data, {backupOnFirstWrite: this._resume_from_crash});
 
     // Once the session file is successfully updated, save the time stamp of the
     // last save and notify the observers.
     promise = promise.then(() => {
       this._lastSaveTime = Date.now();
       Services.obs.notifyObservers(null, "sessionstore-state-write-complete",
         "");
     });
@@ -3973,16 +4014,17 @@ let SessionStoreInternal = {
    */
   _getTabForBrowser: function ssi_getTabForBrowser(aBrowser) {
     let window = aBrowser.ownerDocument.defaultView;
     for (let i = 0; i < window.gBrowser.tabs.length; i++) {
       let tab = window.gBrowser.tabs[i];
       if (tab.linkedBrowser == aBrowser)
         return tab;
     }
+    return undefined;
   },
 
   /**
    * Whether or not to resume session, if not recovering from a crash.
    * @returns bool
    */
   _doResumeSession: function ssi_doResumeSession() {
     return this._prefBranch.getIntPref("startup.page") == 3 ||
@@ -4795,21 +4837,35 @@ function SessionStoreSHistoryListener(aT
   this.tab = aTab;
 }
 SessionStoreSHistoryListener.prototype = {
   QueryInterface: XPCOMUtils.generateQI([
     Ci.nsISHistoryListener,
     Ci.nsISupportsWeakReference
   ]),
   browser: null,
-  OnHistoryNewEntry: function(aNewURI) { },
-  OnHistoryGoBack: function(aBackURI) { return true; },
-  OnHistoryGoForward: function(aForwardURI) { return true; },
-  OnHistoryGotoIndex: function(aIndex, aGotoURI) { return true; },
-  OnHistoryPurge: function(aNumEntries) { return true; },
+// The following events (with the exception of OnHistoryPurge)
+// accompany either a "load" or a "pageshow" which will in turn cause
+// invalidations.
+  OnHistoryNewEntry: function(aNewURI) {
+
+  },
+  OnHistoryGoBack: function(aBackURI) {
+    return true;
+  },
+  OnHistoryGoForward: function(aForwardURI) {
+    return true;
+  },
+  OnHistoryGotoIndex: function(aIndex, aGotoURI) {
+    return true;
+  },
+  OnHistoryPurge: function(aNumEntries) {
+    TabStateCache.delete(this.tab);
+    return true;
+  },
   OnHistoryReload: function(aReloadURI, aReloadFlags) {
     // On reload, we want to make sure that session history loads the right
     // URI. In order to do that, we will juet call restoreTab. That will remove
     // the history listener and load the right URI.
     SessionStoreInternal.restoreTab(this.tab);
     // Returning false will stop the load that docshell is attempting.
     return false;
   }
@@ -4822,9 +4878,111 @@ String.prototype.hasRootDomain = functio
     return false;
 
   if (this == aDomain)
     return true;
 
   let prevChar = this[index - 1];
   return (index == (this.length - aDomain.length)) &&
          (prevChar == "." || prevChar == "/");
+};
+
+function TabData(obj = null) {
+  if (obj) {
+    if (obj instanceof TabData) {
+      // FIXME: Can we get rid of this?
+      return obj;
+    }
+    for (let [key, value] in Iterator(obj)) {
+      this[key] = value;
+    }
+  }
+  return this;
 }
+
+/**
+ * A cache for tabs data.
+ *
+ * This cache implements a weak map from tabs (as XUL elements)
+ * to tab data (as instances of TabData).
+ *
+ * Note that we should never cache private data, as:
+ * - that data is used very seldom by SessionStore;
+ * - caching private data in addition to public data is memory consuming.
+ */
+let TabStateCache = {
+  _data: new WeakMap(),
+
+  /**
+   * Add or replace an entry in the cache.
+   *
+   * @param {XULElement} aTab The key, which may be either a tab
+   * or the corresponding browser. The binding will disappear
+   * if the tab/browser is destroyed.
+   * @param {TabData} aValue The data associated to |aTab|.
+   */
+  set: function(aTab, aValue) {
+    let key = this._normalizeToBrowser(aTab);
+    if (!(aValue instanceof TabData)) {
+      throw new TypeError("Attempting to cache a non TabData");
+    }
+    this._data.set(key, aValue);
+  },
+
+  /**
+   * Return the tab data associated with a tab.
+   *
+   * @param {XULElement} aKey The tab or the associated browser.
+   *
+   * @return {TabData|undefined} The data if available, |undefined|
+   * otherwise.
+   */
+  get: function(aKey) {
+    let key = this._normalizeToBrowser(aKey);
+    return this._data.get(key);
+  },
+
+  /**
+   * Delete the tab data associated with a tab.
+   *
+   * @param {XULElement} aKey The tab or the associated browser.
+   *
+   * Noop of there is no tab data associated with the tab.
+   */
+  delete: function(aKey) {
+    let key = this._normalizeToBrowser(aKey);
+    this._data.delete(key);
+  },
+
+  /**
+   * Delete all tab data.
+   */
+  clear: function() {
+    this._data.clear();
+  },
+
+  /**
+   * Update in place a piece of data.
+   *
+   * @param {XULElement} aKey The tab or the associated browser.
+   * If the tab/browser is not present, do nothing.
+   * @param {string} aField The field to update.
+   * @param {*} aValue The new value to place in the field.
+   */
+  update: function(aKey, aField, aValue) {
+    let key = this._normalizeToBrowser(aKey);
+    let data = this._data.get(key);
+    if (data) {
+      data[aField] = aValue;
+    }
+  },
+
+  _normalizeToBrowser: function(aKey) {
+    let nodeName = aKey.localName;
+    if (nodeName == "tab") {
+      return aKey.linkedBrowser;
+    }
+    if (nodeName == "browser") {
+      return aKey;
+    }
+    throw new TypeError("Key is neither a tab nor a browser: " + nodeName);
+  }
+};
--- a/browser/components/sessionstore/src/SessionWorker.js
+++ b/browser/components/sessionstore/src/SessionWorker.js
@@ -52,16 +52,22 @@ self.onmessage = function (msg) {
 let Agent = {
   // The initial session string as read from disk.
   initialState: null,
 
   // Boolean that tells whether we already wrote
   // the loadState to disk once after startup.
   hasWrittenLoadStateOnce: false,
 
+  // Boolean that tells whether we already made a
+  // call to write(). We will only attempt to move
+  // sessionstore.js to sessionstore.bak on the
+  // first write.
+  hasWrittenState: false,
+
   // The path to sessionstore.js
   path: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js"),
 
   // The path to sessionstore.bak
   backupPath: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak"),
 
   /**
    * This method is only intended to be called by _SessionFile.syncRead() and
@@ -102,17 +108,29 @@ let Agent = {
 
     // No sessionstore data files found. Return an empty string.
     return "";
   },
 
   /**
    * Write the session to disk.
    */
-  write: function (stateString) {
+  write: function (stateString, options) {
+    if (!this.hasWrittenState) {
+      if (options && options.backupOnFirstWrite) {
+        try {
+          File.move(this.path, this.backupPath);
+        } catch (ex if isNoSuchFileEx(ex)) {
+          // Ignore exceptions about non-existent files.
+        }
+      }
+
+      this.hasWrittenState = true;
+    }
+
     let bytes = Encoder.encode(stateString);
     return File.writeAtomic(this.path, bytes, {tmpPath: this.path + ".tmp"});
   },
 
   /**
    * Writes the session state to disk again but changes session.state to
    * 'running' before doing so. This is intended to be called only once, shortly
    * after startup so that we detect crashes on startup correctly.
@@ -135,29 +153,18 @@ let Agent = {
     try {
       state = JSON.parse(this.initialState);
     } finally {
       this.initialState = null;
     }
 
     state.session = state.session || {};
     state.session.state = loadState;
-    return this.write(JSON.stringify(state));
-  },
-
-  /**
-   * Moves sessionstore.js to sessionstore.bak.
-   */
-  moveToBackupPath: function () {
-    try {
-      return File.move(this.path, this.backupPath);
-    } catch (ex if isNoSuchFileEx(ex)) {
-      // Ignore exceptions about non-existent files.
-      return true;
-    }
+    let bytes = Encoder.encode(JSON.stringify(state));
+    return File.writeAtomic(this.path, bytes, {tmpPath: this.path + ".tmp"});
   },
 
   /**
    * Creates a copy of sessionstore.js.
    */
   createBackupCopy: function (ext) {
     try {
       return File.copy(this.path, this.backupPath + ext);
--- a/browser/components/sessionstore/src/_SessionFile.jsm
+++ b/browser/components/sessionstore/src/_SessionFile.jsm
@@ -62,34 +62,28 @@ this._SessionFile = {
     Deprecated.warning(
       "syncRead is deprecated and will be removed in a future version",
       "https://bugzilla.mozilla.org/show_bug.cgi?id=532150")
     return SessionFileInternal.syncRead();
   },
   /**
    * Write the contents of the session file, asynchronously.
    */
-  write: function (aData) {
-    return SessionFileInternal.write(aData);
+  write: function (aData, aOptions = {}) {
+    return SessionFileInternal.write(aData, aOptions);
   },
   /**
    * Writes the initial state to disk again only to change the session's load
    * state. This must only be called once, it will throw an error otherwise.
    */
   writeLoadStateOnceAfterStartup: function (aLoadState) {
     return SessionFileInternal.writeLoadStateOnceAfterStartup(aLoadState);
   },
   /**
    * Create a backup copy, asynchronously.
-   */
-  moveToBackupPath: function () {
-    return SessionFileInternal.moveToBackupPath();
-  },
-  /**
-   * Create a backup copy, asynchronously.
    * This is designed to perform backup on upgrade.
    */
   createBackupCopy: function (ext) {
     return SessionFileInternal.createBackupCopy(ext);
   },
   /**
    * Remove a backup copy, asynchronously.
    * This is designed to clean up a backup on upgrade.
@@ -207,24 +201,24 @@ let SessionFileInternal = {
     SessionWorker.post("setInitialState", [text]);
     return text;
   },
 
   read: function () {
     return SessionWorker.post("read").then(msg => msg.ok);
   },
 
-  write: function (aData) {
+  write: function (aData, aOptions) {
     let refObj = {};
     return TaskUtils.spawn(function task() {
       TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj);
       TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
 
       try {
-        let promise = SessionWorker.post("write", [aData]);
+        let promise = SessionWorker.post("write", [aData, aOptions]);
         // At this point, we measure how long we stop the main thread
         TelemetryStopwatch.finish("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
 
         // Now wait for the result and measure how long we had to wait for the result
         yield promise;
         TelemetryStopwatch.finish("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj);
       } catch (ex) {
         TelemetryStopwatch.cancel("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
@@ -234,20 +228,16 @@ let SessionFileInternal = {
       }
     }.bind(this));
   },
 
   writeLoadStateOnceAfterStartup: function (aLoadState) {
     return SessionWorker.post("writeLoadStateOnceAfterStartup", [aLoadState]);
   },
 
-  moveToBackupPath: function () {
-    return SessionWorker.post("moveToBackupPath");
-  },
-
   createBackupCopy: function (ext) {
     return SessionWorker.post("createBackupCopy", [ext]);
   },
 
   removeBackupCopy: function (ext) {
     return SessionWorker.post("removeBackupCopy", [ext]);
   },
 
--- a/browser/components/sessionstore/test/Makefile.in
+++ b/browser/components/sessionstore/test/Makefile.in
@@ -22,16 +22,17 @@ MOCHITEST_BROWSER_FILES = \
 	browser_dying_cache.js \
 	browser_form_restore_events.js \
 	browser_form_restore_events_sample.html \
 	browser_formdata_format.js \
 	browser_formdata_format_sample.html \
 	browser_input.js \
 	browser_input_sample.html \
 	browser_pageshow.js \
+	browser_sessionStorage.js \
         browser_upgrade_backup.js \
 	browser_windowRestore_perwindowpb.js \
 	browser_248970_b_perwindowpb.js \
 	browser_248970_b_sample.html \
 	browser_339445.js \
 	browser_339445_sample.html \
 	browser_345898.js \
 	browser_346337.js \
--- a/browser/components/sessionstore/test/browser_625257.js
+++ b/browser/components/sessionstore/test/browser_625257.js
@@ -1,85 +1,86 @@
 /* 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/. */
 
+let Scope = {};
+Cu.import("resource://gre/modules/Task.jsm", Scope);
+Cu.import("resource://gre/modules/Promise.jsm", Scope);
+let {Task, Promise} = Scope;
+
+
 // This tests that a tab which is closed while loading is not lost.
 // Specifically, that session store does not rely on an invalid cache when
 // constructing data for a tab which is loading.
 
-// The newly created tab which we load a URL into and try closing/undoing.
-let tab;
-
 // This test steps through the following parts:
 //  1. Tab has been created is loading URI_TO_LOAD.
 //  2. Before URI_TO_LOAD finishes loading, browser.currentURI has changed and
 //     tab is scheduled to be removed.
 //  3. After the tab has been closed, undoCloseTab() has been called and the tab
 //     should fully load.
 const URI_TO_LOAD = "about:mozilla";
 
+function waitForLoadStarted(aTab) {
+  let deferred = Promise.defer();
+  waitForContentMessage(aTab.linkedBrowser,
+    "SessionStore:loadStart",
+    1000,
+    deferred.resolve);
+  return deferred.promise;
+}
+
+function waitForTabLoaded(aTab) {
+  let deferred = Promise.defer();
+  whenBrowserLoaded(aTab.linkedBrowser, deferred.resolve);
+  return deferred.promise;
+}
+
+function waitForTabClosed() {
+  let deferred = Promise.defer();
+  let observer = function() {
+    gBrowser.tabContainer.removeEventListener("TabClose", observer, true);
+    deferred.resolve();
+  };
+  gBrowser.tabContainer.addEventListener("TabClose", observer, true);
+  return deferred.promise;
+}
+
 function test() {
   waitForExplicitFinish();
 
-  gBrowser.addTabsProgressListener(tabsListener);
-
-  tab = gBrowser.addTab();
-
-  tab.linkedBrowser.addEventListener("load", firstOnLoad, true);
-
-  gBrowser.tabContainer.addEventListener("TabClose", onTabClose, true);
-}
-
-function firstOnLoad(aEvent) {
-  tab.linkedBrowser.removeEventListener("load", firstOnLoad, true);
+  Task.spawn(function() {
+    try {
+      // Open a new tab
+      let tab = gBrowser.addTab("about:blank");
+      yield waitForTabLoaded(tab);
 
-  let uri = aEvent.target.location;
-  is(uri, "about:blank", "first load should be for about:blank");
-
-  // Trigger a save state.
-  ss.getBrowserState();
+      // Trigger a save state, to initialize any caches
+      ss.getBrowserState();
 
-  is(gBrowser.tabs[1], tab, "newly created tab should exist by now");
-  ok(tab.linkedBrowser.__SS_data, "newly created tab should be in save state");
+      is(gBrowser.tabs[1], tab, "newly created tab should exist by now");
+      ok(tab.linkedBrowser.__SS_data, "newly created tab should be in save state");
 
-  tab.linkedBrowser.loadURI(URI_TO_LOAD);
-}
+      // Start a load and interrupt it by closing the tab
+      tab.linkedBrowser.loadURI(URI_TO_LOAD);
+      let loaded = yield waitForLoadStarted(tab);
+      ok(loaded, "Load started");
 
-let tabsListener = {
-  onLocationChange: function onLocationChange(aBrowser) {
-    gBrowser.removeTabsProgressListener(tabsListener);
-
-    is(aBrowser.currentURI.spec, URI_TO_LOAD,
-       "should occur after about:blank load and be loading next page");
-
-    // Since we are running in the context of tabs listeners, we do not
-    // want to disrupt other tabs listeners.
-    executeSoon(function() {
+      let tabClosing = waitForTabClosed();
       gBrowser.removeTab(tab);
-    });
-  }
-};
+      info("Now waiting for TabClose to close");
+      yield tabClosing;
 
-function onTabClose(aEvent) {
-  gBrowser.tabContainer.removeEventListener("TabClose", onTabClose, true);
+      // Undo the tab, ensure that it proceeds with loading
+      tab = ss.undoCloseTab(window, 0);
+      yield waitForTabLoaded(tab);
+      is(tab.linkedBrowser.currentURI.spec, URI_TO_LOAD, "loading proceeded as expected");
 
-  is(tab.linkedBrowser.currentURI.spec, URI_TO_LOAD,
-     "should only remove when loading page");
+      gBrowser.removeTab(tab);
 
-  executeSoon(function() {
-    tab = ss.undoCloseTab(window, 0);
-    tab.linkedBrowser.addEventListener("load", secondOnLoad, true);
+      executeSoon(finish);
+    } catch (ex) {
+      ok(false, ex);
+      info(ex.stack);
+    }
   });
 }
-
-function secondOnLoad(aEvent) {
-  let uri = aEvent.target.location;
-  is(uri, URI_TO_LOAD, "should load page from undoCloseTab");
-  done();
-}
-
-function done() {
-  tab.linkedBrowser.removeEventListener("load", secondOnLoad, true);
-  gBrowser.removeTab(tab);
-
-  executeSoon(finish);
-}
--- a/browser/components/sessionstore/test/browser_833286_atomic_backup.js
+++ b/browser/components/sessionstore/test/browser_833286_atomic_backup.js
@@ -1,12 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // This tests are for a sessionstore.js atomic backup.
+// Each test will wait for a write to the Session Store
+// before executing.
 
 let tmp = {};
 Cu.import("resource://gre/modules/osfile.jsm", tmp);
 Cu.import("resource://gre/modules/Task.jsm", tmp);
 Cu.import("resource:///modules/sessionstore/_SessionFile.jsm", tmp);
 
 const {OS, Task, _SessionFile} = tmp;
 
@@ -18,17 +20,17 @@ const backupPath = OS.Path.join(OS.Const
 
 // A text decoder.
 let gDecoder = new TextDecoder();
 // Global variables that contain sessionstore.js and sessionstore.bak data for
 // comparison between tests.
 let gSSData;
 let gSSBakData;
 
-// waitForSaveStateComplete waits for a state write completion.
+// Wait for a state write to complete and then execute a callback.
 function waitForSaveStateComplete(aSaveStateCallback) {
   let topic = "sessionstore-state-write-complete";
 
   function observer() {
     Services.prefs.clearUserPref(PREF_SS_INTERVAL);
     Services.obs.removeObserver(observer, topic);
     executeSoon(function taskCallback() {
       Task.spawn(aSaveStateCallback);
@@ -36,61 +38,56 @@ function waitForSaveStateComplete(aSaveS
   }
 
   Services.obs.addObserver(observer, topic, false);
 }
 
 // Register next test callback and trigger state saving change.
 function nextTest(testFunc) {
   waitForSaveStateComplete(testFunc);
+
+  // We set the interval for session store state saves to be zero
+  // to cause a save ASAP.
   Services.prefs.setIntPref(PREF_SS_INTERVAL, 0);
 }
 
 registerCleanupFunction(function() {
   // Cleaning up after the test: removing the sessionstore.bak file.
   Task.spawn(function cleanupTask() {
     yield OS.File.remove(backupPath);
   });
 });
 
 function test() {
   waitForExplicitFinish();
-  nextTest(testInitialWriteNoBackup);
+  nextTest(testAfterFirstWrite);
 }
 
-function testInitialWriteNoBackup() {
-  // Ensure that sessionstore.js is created, but not sessionstore.bak.
-  let ssExists = yield OS.File.exists(path);
-  let ssBackupExists = yield OS.File.exists(backupPath);
-  ok(ssExists, "sessionstore.js should be created.");
-  ok(!ssBackupExists, "sessionstore.bak should not have been created, yet.");
-
-  nextTest(testWriteNoBackup);
-}
-
-function testWriteNoBackup() {
-  // Ensure sessionstore.bak is not created.
+function testAfterFirstWrite() {
+  // Ensure sessionstore.bak is not created. We start with a clean
+  // profile so there was nothing to move to sessionstore.bak before
+  // initially writing sessionstore.js
   let ssExists = yield OS.File.exists(path);
   let ssBackupExists = yield OS.File.exists(backupPath);
   ok(ssExists, "sessionstore.js should exist.");
   ok(!ssBackupExists, "sessionstore.bak should not have been created, yet");
 
   // Save sessionstore.js data to compare to the sessionstore.bak data in the
   // next test.
   let array = yield OS.File.read(path);
   gSSData = gDecoder.decode(array);
 
-  // Manually trigger _SessionFile.moveToBackupPath since the backup once
-  // promise is already resolved and backup would not be triggered again.
-  yield _SessionFile.moveToBackupPath();
+  // Manually move to the backup since the first write has already happened
+  // and a backup would not be triggered again.
+  yield OS.File.move(path, backupPath);
 
-  nextTest(testWriteBackup);
+  nextTest(testReadBackup);
 }
 
-function testWriteBackup() {
+function testReadBackup() {
   // Ensure sessionstore.bak is finally created.
   let ssExists = yield OS.File.exists(path);
   let ssBackupExists = yield OS.File.exists(backupPath);
   ok(ssExists, "sessionstore.js exists.");
   ok(ssBackupExists, "sessionstore.bak should now be created.");
 
   // Read sessionstore.bak data.
   let array = yield OS.File.read(backupPath);
@@ -122,20 +119,21 @@ function testWriteBackup() {
   ssDataRead = yield _SessionFile.read();
   is(ssDataRead, gSSBakData,
     "_SessionFile.read read sessionstore.bak correctly.");
 
   // Read sessionstore.bak with _SessionFile.syncRead.
   ssDataRead = _SessionFile.syncRead();
   is(ssDataRead, gSSBakData,
     "_SessionFile.syncRead read sessionstore.bak correctly.");
-  nextTest(testNoWriteBackup);
+
+  nextTest(testBackupUnchanged);
 }
 
-function testNoWriteBackup() {
+function testBackupUnchanged() {
   // Ensure sessionstore.bak is backed up only once.
 
   // Read sessionstore.bak data.
   let array = yield OS.File.read(backupPath);
   let ssBakData = gDecoder.decode(array);
   // Ensure the sessionstore.bak did not change.
   is(ssBakData, gSSBakData, "sessionstore.bak is unchanged.");
 
--- a/browser/components/sessionstore/test/browser_capabilities.js
+++ b/browser/components/sessionstore/test/browser_capabilities.js
@@ -25,16 +25,21 @@ function runTests() {
   let state = JSON.parse(ss.getTabState(tab));
   ok(!("disallow" in state), "everything allowed by default");
   ok(flags.every(f => docShell[f]), "all flags set to true");
 
   // Flip a couple of allow* flags.
   docShell.allowImages = false;
   docShell.allowMetaRedirects = false;
 
+  // Now reload the document to ensure that these capabilities
+  // are taken into account
+  browser.reload();
+  yield whenBrowserLoaded(browser);
+
   // Check that we correctly save disallowed features.
   let disallowedState = JSON.parse(ss.getTabState(tab));
   let disallow = new Set(disallowedState.disallow.split(","));
   ok(disallow.has("Images"), "images not allowed");
   ok(disallow.has("MetaRedirects"), "meta redirects not allowed");
   is(disallow.size, 2, "two capabilities disallowed");
 
   // Reuse the tab to restore a new, clean state into it.
@@ -47,17 +52,17 @@ function runTests() {
   ok(flags.every(f => docShell[f]), "all flags set to true");
 
   // Restore the state with disallowed features.
   ss.setTabState(tab, JSON.stringify(disallowedState));
   yield waitForLoad(browser);
 
   // Check that docShell flags are set.
   ok(!docShell.allowImages, "images not allowed");
-  ok(!docShell.allowMetaRedirects, "meta redirects not allowed")
+  ok(!docShell.allowMetaRedirects, "meta redirects not allowed");
 
   // Check that we correctly restored features as disabled.
   state = JSON.parse(ss.getTabState(tab));
   disallow = new Set(state.disallow.split(","));
   ok(disallow.has("Images"), "images not allowed anymore");
   ok(disallow.has("MetaRedirects"), "meta redirects not allowed anymore");
   is(disallow.size, 2, "two capabilities disallowed");
 
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_sessionStorage.js
@@ -0,0 +1,91 @@
+/* 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/. */
+
+let Scope = {};
+Cu.import("resource://gre/modules/Task.jsm", Scope);
+Cu.import("resource://gre/modules/Promise.jsm", Scope);
+let {Task, Promise} = Scope;
+
+function promiseBrowserLoaded(aBrowser) {
+  let deferred = Promise.defer();
+  whenBrowserLoaded(aBrowser, () => deferred.resolve());
+  return deferred.promise;
+}
+
+function forceWriteState() {
+  let deferred = Promise.defer();
+  const PREF = "browser.sessionstore.interval";
+  const TOPIC = "sessionstore-state-write";
+
+  Services.obs.addObserver(function observe() {
+    Services.obs.removeObserver(observe, TOPIC);
+    Services.prefs.clearUserPref(PREF);
+    deferred.resolve();
+  }, TOPIC, false);
+
+  Services.prefs.setIntPref(PREF, 0);
+  return deferred.promise;
+}
+
+function waitForStorageChange(aTab) {
+  let deferred = Promise.defer();
+  waitForContentMessage(aTab.linkedBrowser,
+    "SessionStore:MozStorageChanged",
+    1000,
+    deferred.resolve);
+  return deferred.promise;
+}
+
+function test() {
+
+  waitForExplicitFinish();
+
+  let tab;
+  Task.spawn(function() {
+    try {
+      tab = gBrowser.addTab("http://example.com");
+      // about:home supports sessionStorage and localStorage
+
+      let win = tab.linkedBrowser.contentWindow;
+
+      // Flush loading and next save, call getBrowserState()
+      // a few times to ensure that everything is cached.
+      yield promiseBrowserLoaded(tab.linkedBrowser);
+      yield forceWriteState();
+      info("Calling getBrowserState() to populate cache");
+      ss.getBrowserState();
+
+      info("Change sessionStorage, ensure that state is saved");
+      win.sessionStorage["SESSION_STORAGE_KEY"] = "SESSION_STORAGE_VALUE";
+      let storageChanged = yield waitForStorageChange(tab);
+      ok(storageChanged, "Changing sessionStorage triggered the right message");
+      yield forceWriteState();
+
+      let state = ss.getBrowserState();
+      ok(state.indexOf("SESSION_STORAGE_KEY") != -1, "Key appears in state");
+      ok(state.indexOf("SESSION_STORAGE_VALUE") != -1, "Value appears in state");
+
+
+      info("Change localStorage, ensure that state is not saved");
+      win.localStorage["LOCAL_STORAGE_KEY"] = "LOCAL_STORAGE_VALUE";
+      storageChanged = yield waitForStorageChange(tab);
+      ok(!storageChanged, "Changing localStorage did not trigger a message");
+      yield forceWriteState();
+
+      state = ss.getBrowserState();
+      ok(state.indexOf("LOCAL_STORAGE_KEY") == -1, "Key does not appear in state");
+      ok(state.indexOf("LOCAL_STORAGE_VALUE") == -1, "Value does not appear in state");
+    } catch (ex) {
+      ok(false, ex);
+      info(ex.stack);
+    } finally {
+      // clean up
+      if (tab) {
+        gBrowser.removeTab(tab);
+      }
+
+      executeSoon(finish);
+    }
+  });
+}
--- a/browser/components/sessionstore/test/head.js
+++ b/browser/components/sessionstore/test/head.js
@@ -151,53 +151,100 @@ function waitForTabState(aTab, aState, a
   registerCleanupFunction(function() {
     if (listening) {
       aTab.removeEventListener("SSTabRestored", onSSTabRestored, false);
     }
   });
   ss.setTabState(aTab, JSON.stringify(aState));
 }
 
-// waitForSaveState waits for a state write but not necessarily for the state to
-// turn dirty.
-function waitForSaveState(aSaveStateCallback) {
+/**
+ * Wait for a content -> chrome message.
+ */
+function waitForContentMessage(aBrowser, aTopic, aTimeout, aCallback) {
+  let mm = aBrowser.messageManager;
   let observing = false;
-  let topic = "sessionstore-state-write";
-
-  let sessionSaveTimeout = 1000 +
-    Services.prefs.getIntPref("browser.sessionstore.interval");
-
   function removeObserver() {
     if (!observing)
       return;
-    Services.obs.removeObserver(observer, topic);
+    mm.removeMessageListener(aTopic, observer);
     observing = false;
   }
 
   let timeout = setTimeout(function () {
     removeObserver();
-    aSaveStateCallback();
-  }, sessionSaveTimeout);
+    aCallback(false);
+  }, aTimeout);
 
   function observer(aSubject, aTopic, aData) {
     removeObserver();
     timeout = clearTimeout(timeout);
-    executeSoon(aSaveStateCallback);
+    executeSoon(() => aCallback(true));
   }
 
   registerCleanupFunction(function() {
     removeObserver();
     if (timeout) {
       clearTimeout(timeout);
     }
   });
 
   observing = true;
-  Services.obs.addObserver(observer, topic, false);
-};
+  mm.addMessageListener(aTopic, observer);
+}
+
+function waitForTopic(aTopic, aTimeout, aCallback) {
+  let observing = false;
+  function removeObserver() {
+    if (!observing)
+      return;
+    Services.obs.removeObserver(observer, aTopic);
+    observing = false;
+  }
+
+  let timeout = setTimeout(function () {
+    removeObserver();
+    aCallback(false);
+  }, aTimeout);
+
+  function observer(aSubject, aTopic, aData) {
+    removeObserver();
+    timeout = clearTimeout(timeout);
+    executeSoon(() => aCallback(true));
+  }
+
+  registerCleanupFunction(function() {
+    removeObserver();
+    if (timeout) {
+      clearTimeout(timeout);
+    }
+  });
+
+  observing = true;
+  Services.obs.addObserver(observer, aTopic, false);
+}
+
+/**
+ * Wait until session restore has finished collecting its data and is
+ * getting ready to write that data ("sessionstore-state-write").
+ *
+ * This function is meant to be called immediately after the code
+ * that will trigger the saving.
+ *
+ * Note that this does not wait for the disk write to be complete.
+ *
+ * @param {function} aCallback If sessionstore-state-write is sent
+ * within buffering interval + 100 ms, the callback is passed |true|,
+ * otherwise, it is passed |false|.
+ */
+function waitForSaveState(aCallback) {
+  let timeout = 100 +
+    Services.prefs.getIntPref("browser.sessionstore.interval");
+  return waitForTopic("sessionstore-state-write", timeout, aCallback);
+}
 
 function whenBrowserLoaded(aBrowser, aCallback = next) {
   aBrowser.addEventListener("load", function onLoad() {
     aBrowser.removeEventListener("load", onLoad, true);
     executeSoon(aCallback);
   }, true);
 }
 
@@ -330,23 +377,21 @@ let TestRunner = {
   backupState: {},
 
   /**
    * Starts the test runner.
    */
   run: function () {
     waitForExplicitFinish();
 
-    SessionStore.promiseInitialized.then(function () {
-      executeSoon(function () {
-        this.backupState = JSON.parse(ss.getBrowserState());
-        this._iter = runTests();
-        this.next();
-      }.bind(this));
-    }.bind(this));
+    SessionStore.promiseInitialized.then(() => {
+      this.backupState = JSON.parse(ss.getBrowserState());
+      this._iter = runTests();
+      this.next();
+    });
   },
 
   /**
    * Runs the next available test or finishes if there's no test left.
    */
   next: function () {
     try {
       TestRunner._iter.next();
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/unit/test_backup_once.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let toplevel = this;
+Cu.import("resource://gre/modules/osfile.jsm");
+
+function run_test() {
+  let profd = do_get_profile();
+  Cu.import("resource:///modules/sessionstore/_SessionFile.jsm", toplevel);
+  decoder = new TextDecoder();
+  pathStore = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js");
+  pathBackup = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak");
+  let source = do_get_file("data/sessionstore_valid.js");
+  source.copyTo(profd, "sessionstore.js");
+  run_next_test();
+}
+
+let pathStore;
+let pathBackup;
+let decoder;
+
+// Write to the store, and check that a backup is created first
+add_task(function test_first_write_backup() {
+  let content = "test_1";
+  let initial_content = decoder.decode(yield OS.File.read(pathStore));
+
+  do_check_true(!(yield OS.File.exists(pathBackup)));
+  yield _SessionFile.write(content, {backupOnFirstWrite: true});
+  do_check_true(yield OS.File.exists(pathBackup));
+
+  let backup_content = decoder.decode(yield OS.File.read(pathBackup));
+  do_check_eq(initial_content, backup_content);
+});
+
+// Write to the store again, and check that the backup is not updated
+add_task(function test_second_write_no_backup() {
+  let content = "test_2";
+  let initial_content = decoder.decode(yield OS.File.read(pathStore));
+  let initial_backup_content = decoder.decode(yield OS.File.read(pathBackup));
+
+  yield _SessionFile.write(content, {backupOnFirstWrite: true});
+
+  let written_content = decoder.decode(yield OS.File.read(pathStore));
+  do_check_eq(content, written_content);
+
+  let backup_content = decoder.decode(yield OS.File.read(pathBackup));
+  do_check_eq(initial_backup_content, backup_content);
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/unit/test_no_backup_first_write.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let toplevel = this;
+Cu.import("resource://gre/modules/osfile.jsm");
+
+function run_test() {
+  let profd = do_get_profile();
+  Cu.import("resource:///modules/sessionstore/_SessionFile.jsm", toplevel);
+  pathStore = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js");
+  pathBackup = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak");
+  let source = do_get_file("data/sessionstore_valid.js");
+  source.copyTo(profd, "sessionstore.js");
+  run_next_test();
+}
+
+let pathStore;
+let pathBackup;
+
+// Write to the store first with |backupOnFirstWrite: false|,
+// and make sure second write does not backup even with
+// |backupOnFirstWrite: true|
+add_task(function test_no_backup_on_second_write() {
+  let content = "test_1";
+
+  do_check_true(!(yield OS.File.exists(pathBackup)));
+  yield _SessionFile.write(content, {backupOnFirstWrite: false});
+  do_check_true(!(yield OS.File.exists(pathBackup)));
+
+  yield _SessionFile.write(content, {backupOnFirstWrite: true});
+  do_check_true(!(yield OS.File.exists(pathBackup)));
+});
--- a/browser/components/sessionstore/test/unit/xpcshell.ini
+++ b/browser/components/sessionstore/test/unit/xpcshell.ini
@@ -1,10 +1,12 @@
 [DEFAULT]
 head = head.js
 tail =
 firefox-appdir = browser
 
 [test_backup.js]
+[test_backup_once.js]
+[test_no_backup_first_write.js]
 [test_startup_nosession_sync.js]
 [test_startup_nosession_async.js]
 [test_startup_session_sync.js]
 [test_startup_session_async.js]
--- a/browser/devtools/debugger/CmdDebugger.jsm
+++ b/browser/devtools/debugger/CmdDebugger.jsm
@@ -419,16 +419,132 @@ gcli.addCommand({
     });
     div.appendChild(ol);
 
     return div;
   }
 });
 
 /**
+ * Define the 'dbg blackbox' and 'dbg unblackbox' commands.
+ */
+[
+  {
+    name: "blackbox",
+    clientMethod: "blackBox",
+    l10nPrefix: "dbgBlackBox"
+  },
+  {
+    name: "unblackbox",
+    clientMethod: "unblackBox",
+    l10nPrefix: "dbgUnBlackBox"
+  }
+].forEach(function (cmd) {
+  const lookup = function (id) {
+    return gcli.lookup(cmd.l10nPrefix + id);
+  };
+
+  gcli.addCommand({
+    name: "dbg " + cmd.name,
+    description: lookup("Desc"),
+    params: [
+      {
+        name: "source",
+        type: {
+          name: "selection",
+          data: function (context) {
+            let dbg = getPanel(context, "jsdebugger");
+            return dbg
+              ? [s for (s of dbg._view.Sources.values)]
+              : [];
+          }
+        },
+        description: lookup("SourceDesc"),
+        defaultValue: null
+      },
+      {
+        name: "glob",
+        type: "string",
+        description: lookup("GlobDesc"),
+        defaultValue: null
+      }
+    ],
+    returnType: "dom",
+    exec: function (args, context) {
+      const dbg = getPanel(context, "jsdebugger");
+      const doc = context.environment.chromeDocument;
+      if (!dbg) {
+        throw new Error(gcli.lookup("debuggerClosed"));
+      }
+
+      const { promise, resolve, reject } = context.defer();
+      const { activeThread } = dbg._controller;
+      const globRegExp = args.glob
+        ? globToRegExp(args.glob)
+        : null;
+
+      // Filter the sources down to those that we will need to black box.
+
+      function shouldBlackBox(source) {
+        return globRegExp && globRegExp.test(source.url)
+          || args.source && source.url == args.source;
+      }
+
+      const toBlackBox = [s.attachment.source
+                          for (s of dbg._view.Sources.items)
+                          if (shouldBlackBox(s.attachment.source))];
+
+      // If we aren't black boxing any sources, bail out now.
+
+      if (toBlackBox.length === 0) {
+        const empty = createXHTMLElement(doc, "div");
+        empty.textContent = lookup("EmptyDesc");
+        return void resolve(empty);
+      }
+
+      // Send the black box request to each source we are black boxing. As we
+      // get responses, accumulate the results in `blackBoxed`.
+
+      const blackBoxed = [];
+
+      for (let source of toBlackBox) {
+        activeThread.source(source)[cmd.clientMethod](function ({ error }) {
+          if (error) {
+            blackBoxed.push(lookup("ErrorDesc") + " " + source.url);
+          } else {
+            blackBoxed.push(source.url);
+          }
+
+          if (toBlackBox.length === blackBoxed.length) {
+            displayResults();
+          }
+        });
+      }
+
+      // List the results for the user.
+
+      function displayResults() {
+        const results = doc.createElement("div");
+        results.textContent = lookup("NonEmptyDesc");
+        const list = createXHTMLElement(doc, "ul");
+        results.appendChild(list);
+        for (let result of blackBoxed) {
+          const item = createXHTMLElement(doc, "li");
+          item.textContent = result;
+          list.appendChild(item);
+        }
+        resolve(results);
+      }
+
+      return promise;
+    }
+  });
+});
+
+/**
  * A helper to create xhtml namespaced elements
  */
 function createXHTMLElement(document, tagname) {
   return document.createElementNS("http://www.w3.org/1999/xhtml", tagname);
 }
 
 /**
  * A helper to go from a command context to a debugger panel
@@ -447,8 +563,37 @@ function getPanel(context, id, options =
     let toolbox = gDevTools.getToolbox(target);
     if (toolbox) {
       return toolbox.getPanel(id);
     } else {
       return undefined;
     }
   }
 }
+
+/**
+ * Converts a glob to a regular expression
+ */
+function globToRegExp(glob) {
+  const reStr = glob
+  // Escape existing regular expression syntax
+    .replace(/\\/g, "\\\\")
+    .replace(/\//g, "\\/")
+    .replace(/\^/g, "\\^")
+    .replace(/\$/g, "\\$")
+    .replace(/\+/g, "\\+")
+    .replace(/\?/g, "\\?")
+    .replace(/\./g, "\\.")
+    .replace(/\(/g, "\\(")
+    .replace(/\)/g, "\\)")
+    .replace(/\=/g, "\\=")
+    .replace(/\!/g, "\\!")
+    .replace(/\|/g, "\\|")
+    .replace(/\{/g, "\\{")
+    .replace(/\}/g, "\\}")
+    .replace(/\,/g, "\\,")
+    .replace(/\[/g, "\\[")
+    .replace(/\]/g, "\\]")
+    .replace(/\-/g, "\\-")
+  // Turn * into the match everything wildcard
+    .replace(/\*/g, ".*")
+  return new RegExp("^" + reStr + "$");
+}
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -443,33 +443,33 @@ StackFrames.prototype = {
    * Connect to the current thread client.
    */
   connect: function() {
     dumpn("StackFrames is connecting...");
     this.activeThread.addListener("paused", this._onPaused);
     this.activeThread.addListener("resumed", this._onResumed);
     this.activeThread.addListener("framesadded", this._onFrames);
     this.activeThread.addListener("framescleared", this._onFramesCleared);
-    window.addEventListener("Debugger:BlackBoxChange", this._onBlackBoxChange, false);
+    this.activeThread.addListener("blackboxchange", this._onBlackBoxChange);
     this._handleTabNavigation();
   },
 
   /**
    * Disconnect from the client.
    */
   disconnect: function() {
     if (!this.activeThread) {
       return;
     }
     dumpn("StackFrames is disconnecting...");
     this.activeThread.removeListener("paused", this._onPaused);
     this.activeThread.removeListener("resumed", this._onResumed);
     this.activeThread.removeListener("framesadded", this._onFrames);
     this.activeThread.removeListener("framescleared", this._onFramesCleared);
-    window.removeEventListener("Debugger:BlackBoxChange", this._onBlackBoxChange, false);
+    this.activeThread.removeListener("blackboxchange", this._onBlackBoxChange);
   },
 
   /**
    * Handles any initialization on a tab navigation event issued by the client.
    */
   _handleTabNavigation: function() {
     dumpn("Handling tab navigation in the StackFrames");
     // Nothing to do here yet.
@@ -635,17 +635,17 @@ StackFrames.prototype = {
     // After each frame step (in, over, out), framescleared is fired, which
     // forces the UI to be emptied and rebuilt on framesadded. Most of the times
     // this is not necessary, and will result in a brief redraw flicker.
     // To avoid it, invalidate the UI only after a short time if necessary.
     window.setTimeout(this._afterFramesCleared, FRAME_STEP_CLEAR_DELAY);
   },
 
   /**
-   * Handler for the debugger's BlackBoxChange notification.
+   * Handler for the debugger's blackboxchange notification.
    */
   _onBlackBoxChange: function() {
     if (this.activeThread.state == "paused") {
       // We have to clear out the existing frames and refetch them to get their
       // updated black boxed status.
       this.activeThread._clearFrames();
       this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE);
     }
@@ -885,44 +885,47 @@ StackFrames.prototype = {
 /**
  * Keeps the source script list up-to-date, using the thread client's
  * source script cache.
  */
 function SourceScripts() {
   this._onNewGlobal = this._onNewGlobal.bind(this);
   this._onNewSource = this._onNewSource.bind(this);
   this._onSourcesAdded = this._onSourcesAdded.bind(this);
+  this._onBlackBoxChange = this._onBlackBoxChange.bind(this);
 }
 
 SourceScripts.prototype = {
   get activeThread() DebuggerController.activeThread,
   get debuggerClient() DebuggerController.client,
   _newSourceTimeout: null,
 
   /**
    * Connect to the current thread client.
    */
   connect: function() {
     dumpn("SourceScripts is connecting...");
     this.debuggerClient.addListener("newGlobal", this._onNewGlobal);
     this.debuggerClient.addListener("newSource", this._onNewSource);
+    this.activeThread.addListener("blackboxchange", this._onBlackBoxChange);
     this._handleTabNavigation();
   },
 
   /**
    * Disconnect from the client.
    */
   disconnect: function() {
     if (!this.activeThread) {
       return;
     }
     dumpn("SourceScripts is disconnecting...");
     window.clearTimeout(this._newSourceTimeout);
     this.debuggerClient.removeListener("newGlobal", this._onNewGlobal);
     this.debuggerClient.removeListener("newSource", this._onNewSource);
+    this.activeThread.removeListener("blackboxchange", this._onBlackBoxChange);
   },
 
   /**
    * Handles any initialization on a tab navigation event issued by the client.
    */
   _handleTabNavigation: function() {
     if (!this.activeThread) {
       return;
@@ -1021,33 +1024,42 @@ SourceScripts.prototype = {
     DebuggerController.Breakpoints.updateEditorBreakpoints();
     DebuggerController.Breakpoints.updatePaneBreakpoints();
 
     // Signal that scripts have been added.
     window.dispatchEvent(document, "Debugger:AfterSourcesAdded");
   },
 
   /**
+   * Handler for the debugger client's 'blackboxchange' notification.
+   */
+  _onBlackBoxChange: function (aEvent, { url, isBlackBoxed }) {
+    const item = DebuggerView.Sources.getItemByValue(url);
+    if (item) {
+      DebuggerView.Sources.callMethod("checkItem", item.target, !isBlackBoxed);
+    }
+  },
+
+  /**
    * Set the black boxed status of the given source.
    *
    * @param Object aSource
    *        The source form.
    * @param bool aBlackBoxFlag
    *        True to black box the source, false to un-black box it.
    */
   blackBox: function(aSource, aBlackBoxFlag) {
     const sourceClient = this.activeThread.source(aSource);
     sourceClient[aBlackBoxFlag ? "blackBox" : "unblackBox"](function({ error, message }) {
       if (error) {
         let msg = "Could not toggle black boxing for "
           + aSource.url + ": " + message;
         dumpn(msg);
         return void Cu.reportError(msg);
       }
-      window.dispatchEvent(document, "Debugger:BlackBoxChange", sourceClient);
     });
   },
 
   /**
    * Gets a specified source's text.
    *
    * @param object aSource
    *        The source object coming from the active thread.
--- a/browser/devtools/debugger/test/Makefile.in
+++ b/browser/devtools/debugger/test/Makefile.in
@@ -13,16 +13,17 @@ include $(DEPTH)/config/autoconf.mk
 MOCHITEST_BROWSER_TESTS = \
 	browser_dbg_aaa_run_first_leaktest.js \
 	browser_dbg_blackboxing-01.js \
 	browser_dbg_blackboxing-02.js \
 	browser_dbg_blackboxing-03.js \
 	browser_dbg_blackboxing-04.js \
 	browser_dbg_clean-exit.js \
 	browser_dbg_cmd.js \
+	browser_dbg_cmd_blackbox.js \
 	browser_dbg_cmd_break.js \
 	browser_dbg_debuggerstatement.js \
 	browser_dbg_listtabs-01.js \
 	browser_dbg_listtabs-02.js \
 	browser_dbg_tabactor-01.js \
 	browser_dbg_tabactor-02.js \
 	browser_dbg_globalactor-01.js \
 	browser_dbg_nav-01.js \
--- a/browser/devtools/debugger/test/browser_dbg_blackboxing-01.js
+++ b/browser/devtools/debugger/test/browser_dbg_blackboxing-01.js
@@ -31,18 +31,18 @@ function test()
 }
 
 function testBlackBoxSource() {
   once(gDebugger, "Debugger:SourceShown", function () {
     const checkbox = gDebugger.document.querySelector(".side-menu-widget-item-checkbox");
     ok(checkbox, "Should get the checkbox for black boxing the source");
     ok(checkbox.checked, "Should not be black boxed by default");
 
-    once(gDebugger, "Debugger:BlackBoxChange", function (event) {
-      const sourceClient = event.detail;
+    const { activeThread } = gDebugger.DebuggerController;
+    activeThread.addOneTimeListener("blackboxchange", function (event, sourceClient) {
       ok(sourceClient.isBlackBoxed, "The source should be black boxed now");
       ok(!checkbox.checked, "The checkbox should no longer be checked.");
 
       testBlackBoxReload();
     });
 
     checkbox.click();
   });
--- a/browser/devtools/debugger/test/browser_dbg_blackboxing-02.js
+++ b/browser/devtools/debugger/test/browser_dbg_blackboxing-02.js
@@ -32,18 +32,18 @@ function test()
   });
 }
 
 function testBlackBoxSource() {
   once(gDebugger, "Debugger:SourceShown", function () {
     const checkbox = getBlackBoxCheckbox(BLACKBOXME_URL);
     ok(checkbox, "Should get the checkbox for blackBoxing the source");
 
-    once(gDebugger, "Debugger:BlackBoxChange", function (event) {
-      const sourceClient = event.detail;
+    const { activeThread } = gDebugger.DebuggerController;
+    activeThread.addOneTimeListener("blackboxchange", function (event, sourceClient) {
       ok(sourceClient.isBlackBoxed, "The source should be black boxed now");
 
       testBlackBoxStack();
     });
 
     checkbox.click();
   });
 }
--- a/browser/devtools/debugger/test/browser_dbg_blackboxing-03.js
+++ b/browser/devtools/debugger/test/browser_dbg_blackboxing-03.js
@@ -50,20 +50,19 @@ function testBlackBoxStack() {
 
   gDebuggee.runTest();
 }
 
 function testBlackBoxSource() {
   const checkbox = getBlackBoxCheckbox(BLACKBOXME_URL);
   ok(checkbox, "Should get the checkbox for black boxing the source");
 
-  once(gDebugger, "Debugger:BlackBoxChange", function (event) {
-    const { activeThread } = gDebugger.DebuggerController;
+  const { activeThread } = gDebugger.DebuggerController;
+  activeThread.addOneTimeListener("blackboxchange", function (event, sourceClient) {
     activeThread.addOneTimeListener("framesadded", function () {
-      const sourceClient = event.detail;
       ok(sourceClient.isBlackBoxed, "The source should be black boxed now");
 
       const frames = gDebugger.DebuggerView.StackFrames.widget._list;
       is(frames.querySelectorAll(".dbg-stackframe").length, 3,
          "Should only get 3 frames");
       is(frames.querySelectorAll(".dbg-stackframe-black-boxed").length, 1,
          "And one of them is the combined black boxed frames");
 
--- a/browser/devtools/debugger/test/browser_dbg_blackboxing-04.js
+++ b/browser/devtools/debugger/test/browser_dbg_blackboxing-04.js
@@ -30,25 +30,24 @@ function test()
     once(gDebugger, "Debugger:SourceShown", function () {
       blackBoxSources();
     });
   });
 }
 
 function blackBoxSources() {
   let timesFired = 0;
-  gDebugger.addEventListener("Debugger:BlackBoxChange", function _onBlackboxChange() {
+  const { activeThread } = gDebugger.DebuggerController;
+  activeThread.addListener("blackboxchange", function _onBlackBoxChange() {
     if (++timesFired !== 3) {
       return;
     }
-    gDebugger.removeEventListener("Debugger:BlackBoxChange", _onBlackboxChange, false);
+    activeThread.removeListener("blackboxchange", _onBlackBoxChange);
 
-    const { activeThread } = gDebugger.DebuggerController;
     activeThread.addOneTimeListener("framesadded", testStackFrames);
-
     gDebuggee.one();
   }, false);
 
   getBlackBoxCheckbox(EXAMPLE_URL + "blackboxing_one.js").click();
   getBlackBoxCheckbox(EXAMPLE_URL + "blackboxing_two.js").click();
   getBlackBoxCheckbox(EXAMPLE_URL + "blackboxing_three.js").click();
 }
 
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_cmd_blackbox.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the 'dbg blackbox' and 'dbg unblackbox' commands work as they
+// should.
+
+const TEST_URL = EXAMPLE_URL + "browser_dbg_blackboxing.html";
+const BLACKBOXME_URL = EXAMPLE_URL + "blackboxing_blackboxme.js";
+const BLACKBOXONE_URL = EXAMPLE_URL + "blackboxing_one.js";
+const BLACKBOXTWO_URL = EXAMPLE_URL + "blackboxing_two.js";
+const BLACKBOXTHREE_URL = EXAMPLE_URL + "blackboxing_three.js";
+
+let gcli = Cu.import("resource://gre/modules/devtools/gcli.jsm", {}).gcli;
+
+let gTarget;
+let gPanel;
+let gOptions;
+let gDebugger;
+let gClient;
+let gThreadClient;
+let gTab;
+
+function cmd(typed, expectedNumEvents=1) {
+  const deferred = promise.defer();
+
+  let timesFired = 0;
+  gThreadClient.addListener("blackboxchange", function _onBlackBoxChange() {
+    if (++timesFired === expectedNumEvents) {
+      gThreadClient.removeListener("blackboxchange", _onBlackBoxChange);
+      deferred.resolve();
+    }
+  });
+
+  helpers.audit(gOptions, [{
+    setup: typed,
+    exec: {}
+  }]);
+
+  return deferred.promise;
+}
+
+function test() {
+  helpers.addTabWithToolbar(TEST_URL, function(options) {
+    gOptions = options;
+    gTarget = options.target;
+    return gDevTools.showToolbox(options.target, "jsdebugger")
+      .then(setupGlobals)
+      .then(waitForDebuggerSources)
+      .then(testBlackBoxSource)
+      .then(testUnBlackBoxSource)
+      .then(testBlackBoxGlob)
+      .then(testUnBlackBoxGlob)
+      .then(null, function (error) {
+        ok(false, "Got an error: " + error.message + "\n" + error.stack);
+      })
+      .then(finishUp);
+  });
+}
+
+function setupGlobals(toolbox) {
+  gTab = gBrowser.selectedTab;
+  gPanel = toolbox.getCurrentPanel();
+  gDebugger = gPanel.panelWin;
+  gClient = gDebugger.gClient;
+  gThreadClient = gClient.activeThread;
+}
+
+function waitForDebuggerSources() {
+  const deferred = promise.defer();
+  gDebugger.addEventListener("Debugger:SourceShown", function _onSourceShown() {
+    gDebugger.removeEventListener("Debugger:SourceShown", _onSourceShown, false);
+    deferred.resolve();
+  }, false);
+  return deferred.promise;
+}
+
+function testBlackBoxSource() {
+  return cmd("dbg blackbox " + BLACKBOXME_URL)
+    .then(function () {
+      const checkbox = getBlackBoxCheckbox(BLACKBOXME_URL);
+      ok(!checkbox.checked,
+         "Should be able to black box a specific source");
+    });
+}
+
+function testUnBlackBoxSource() {
+  return cmd("dbg unblackbox " + BLACKBOXME_URL)
+    .then(function () {
+      const checkbox = getBlackBoxCheckbox(BLACKBOXME_URL);
+      ok(checkbox.checked,
+         "Should be able to stop black boxing a specific source");
+    });
+}
+
+function testBlackBoxGlob() {
+  return cmd("dbg blackbox --glob *blackboxing_t*.js", 2)
+    .then(function () {
+      ok(getBlackBoxCheckbox(BLACKBOXME_URL).checked,
+         "blackboxme should not be black boxed because it doesn't match the glob");
+      ok(getBlackBoxCheckbox(BLACKBOXONE_URL).checked,
+         "blackbox_one should not be black boxed because it doesn't match the glob");
+
+      ok(!getBlackBoxCheckbox(BLACKBOXTWO_URL).checked,
+         "blackbox_two should be black boxed because it matches the glob");
+      ok(!getBlackBoxCheckbox(BLACKBOXTHREE_URL).checked,
+         "blackbox_three should be black boxed because it matches the glob");
+    });
+}
+
+function testUnBlackBoxGlob() {
+  return cmd("dbg unblackbox --glob *blackboxing_t*.js", 2)
+    .then(function () {
+      ok(getBlackBoxCheckbox(BLACKBOXTWO_URL).checked,
+         "blackbox_two should be un-black boxed because it matches the glob");
+      ok(getBlackBoxCheckbox(BLACKBOXTHREE_URL).checked,
+         "blackbox_three should be un-black boxed because it matches the glob");
+    });
+}
+
+function finishUp() {
+  gTarget = null;
+  gPanel = null;
+  gOptions = null;
+  gClient = null;
+  gThreadClient = null;
+  gDebugger = null;
+  closeDebuggerAndFinish();
+}
+
+function getBlackBoxCheckbox(url) {
+  return gDebugger.document.querySelector(
+    ".side-menu-widget-item[tooltiptext=\""
+      + url + "\"] .side-menu-widget-item-checkbox");
+}
--- a/browser/devtools/shared/widgets/SideMenuWidget.jsm
+++ b/browser/devtools/shared/widgets/SideMenuWidget.jsm
@@ -52,16 +52,17 @@ this.SideMenuWidget = function SideMenuW
   this._list.addEventListener("mousedown", e => this.emit("mousePress", e), false);
   this._parent.appendChild(this._list);
   this._boxObject = this._list.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
 
   // Menu items can optionally be grouped.
   this._groupsByName = new Map(); // Can't use a WeakMap because keys are strings.
   this._orderedGroupElementsArray = [];
   this._orderedMenuElementsArray = [];
+  this._itemsByElement = new Map();
 
   // This widget emits events that can be handled in a MenuContainer.
   EventEmitter.decorate(this);
 
   // Delegate some of the associated node's methods to satisfy the interface
   // required by MenuContainer instances.
   ViewHelpers.delegateWidgetEventMethods(this, aNode);
 };
@@ -161,16 +162,17 @@ SideMenuWidget.prototype = {
       aChild.parentNode.remove();
     } else {
       // Groups with no title don't have any special internal structure.
       aChild.remove();
     }
 
     this._orderedMenuElementsArray.splice(
       this._orderedMenuElementsArray.indexOf(aChild), 1);
+    this._itemsByElement.delete(aChild);
 
     if (this._selectedItem == aChild) {
       this._selectedItem = null;
     }
   },
 
   /**
    * Removes all of the child nodes from this container.
@@ -183,16 +185,17 @@ SideMenuWidget.prototype = {
       list.firstChild.remove();
     }
 
     this._selectedItem = null;
 
     this._groupsByName.clear();
     this._orderedGroupElementsArray.length = 0;
     this._orderedMenuElementsArray.length = 0;
+    this._itemsByElement.clear();
   },
 
   /**
    * Gets the currently selected child node in this container.
    * @return nsIDOMNode
    */
   get selectedItem() this._selectedItem,
 
@@ -324,16 +327,32 @@ SideMenuWidget.prototype = {
     this._parent.removeAttribute(aName);
 
     if (aName == "notice") {
       this._removeNotice();
     }
   },
 
   /**
+   * Set the checkbox state for the item associated with the given node.
+   *
+   * @param nsIDOMNode aNode
+   *        The dom node for an item we want to check.
+   * @param boolean aCheckState
+   *        True to check, false to uncheck.
+   */
+  checkItem: function(aNode, aCheckState) {
+    const widgetItem = this._itemsByElement.get(aNode);
+    if (!widgetItem) {
+      throw new Error("No item for " + aNode);
+    }
+    widgetItem.check(aCheckState);
+  },
+
+  /**
    * Sets the text displayed in this container as a notice.
    * @param string aValue
    */
   set notice(aValue) {
     if (this._noticeTextNode) {
       this._noticeTextNode.setAttribute("value", aValue);
     }
     this._noticeTextValue = aValue;
@@ -419,16 +438,17 @@ SideMenuWidget.prototype = {
   _showCheckboxes: false,
   _parent: null,
   _list: null,
   _boxObject: null,
   _selectedItem: null,
   _groupsByName: null,
   _orderedGroupElementsArray: null,
   _orderedMenuElementsArray: null,
+  _itemsByElement: null,
   _ensureVisibleTimeout: null,
   _noticeTextContainer: null,
   _noticeTextNode: null,
   _noticeTextValue: ""
 };
 
 /**
  * A SideMenuGroup constructor for the BreadcrumbsWidget.
@@ -473,16 +493,17 @@ function SideMenuGroup(aWidget, aName) {
     let target = this._target = this._list = this.document.createElement("vbox");
     target.className = "side-menu-widget-group side-menu-widget-group-list";
   }
 }
 
 SideMenuGroup.prototype = {
   get _orderedGroupElementsArray() this.ownerView._orderedGroupElementsArray,
   get _orderedMenuElementsArray() this.ownerView._orderedMenuElementsArray,
+  get _itemsByElement() { return this.ownerView._itemsByElement; },
 
   /**
    * Inserts this group in the parent container at the specified index.
    *
    * @param number aIndex
    *        The position in the container intended for this group.
    */
   insertSelfAt: function(aIndex) {
@@ -544,41 +565,28 @@ SideMenuGroup.prototype = {
  * @param object aAttachment [optional]
  *        The attachment object.
  */
 function SideMenuItem(aGroup, aContents, aTooltip, aArrowFlag, aCheckboxFlag, aAttachment={}) {
   this.document = aGroup.document;
   this.window = aGroup.window;
   this.ownerView = aGroup;
 
-  let makeCheckbox = () => {
-    let checkbox = this.document.createElement("checkbox");
-    checkbox.className = "side-menu-widget-item-checkbox";
-    checkbox.setAttribute("checked", aAttachment.checkboxState);
-    checkbox.setAttribute("tooltiptext", aAttachment.checkboxTooltip);
-    checkbox.addEventListener("command", function () {
-      ViewHelpers.dispatchEvent(checkbox, "check", {
-        checked: checkbox.checked,
-      });
-    }, false);
-    return checkbox;
-  };
-
   if (aArrowFlag || aCheckboxFlag) {
     let container = this._container = this.document.createElement("hbox");
     container.className = "side-menu-widget-item";
     container.setAttribute("tooltiptext", aTooltip);
     container.setAttribute("align", "start");
 
     let target = this._target = this.document.createElement("vbox");
     target.className = "side-menu-widget-item-contents";
 
     // Show a checkbox before the content.
     if (aCheckboxFlag) {
-      let checkbox = this._checkbox = makeCheckbox();
+      let checkbox = this._checkbox = this._makeCheckbox(aAttachment);
       container.appendChild(checkbox);
     }
 
     container.appendChild(target);
 
     // Show a horizontal arrow towards the content.
     if (aArrowFlag) {
       let arrow = this._arrow = this.document.createElement("hbox");
@@ -594,16 +602,46 @@ function SideMenuItem(aGroup, aContents,
 
   this._target.setAttribute("flex", "1");
   this.contents = aContents;
 }
 
 SideMenuItem.prototype = {
   get _orderedGroupElementsArray() this.ownerView._orderedGroupElementsArray,
   get _orderedMenuElementsArray() this.ownerView._orderedMenuElementsArray,
+  get _itemsByElement() { return this.ownerView._itemsByElement; },
+
+  /**
+   * Create the checkbox used when the checkbox flag is true. Emits a "check"
+   * event whenever the checkbox is checked or unchecked by the user.
+   *
+   * @param Object aAttachment
+   *        The attachment object. The following properties are used:
+   *          - checkboxState: true for checked, false for unchecked
+   8          - checkboxTooltip: The tooltip text of the checkbox
+   */
+  _makeCheckbox: function (aAttachment) {
+    let checkbox = this.document.createElement("checkbox");
+    checkbox.className = "side-menu-widget-item-checkbox";
+    checkbox.setAttribute("tooltiptext", aAttachment.checkboxTooltip);
+
+    if (aAttachment.checkboxState) {
+      checkbox.setAttribute("checked", true);
+    } else {
+      checkbox.removeAttribute("checked");
+    }
+
+    checkbox.addEventListener("command", function () {
+      ViewHelpers.dispatchEvent(checkbox, "check", {
+        checked: checkbox.checked,
+      });
+    }, false);
+
+    return checkbox;
+  },
 
   /**
    * Inserts this item in the parent group at the specified index.
    *
    * @param number aIndex
    *        The position in the container intended for this item.
    * @return nsIDOMNode
    *         The element associated with the displayed item.
@@ -614,21 +652,39 @@ SideMenuItem.prototype = {
 
     if (aIndex >= 0) {
       ownerList.insertBefore(this._container, ownerList.childNodes[aIndex]);
       menuArray.splice(aIndex, 0, this._target);
     } else {
       ownerList.appendChild(this._container);
       menuArray.push(this._target);
     }
+    this._itemsByElement.set(this._target, this);
 
     return this._target;
   },
 
   /**
+   * Check or uncheck the checkbox associated with this item.
+   *
+   * @param boolean aCheckState
+   *        True to check, false to uncheck.
+   */
+  check: function(aCheckState) {
+    if (!this._checkbox) {
+      throw new Error("Cannot check items that do not have checkboxes.");
+    }
+    if (aCheckState) {
+      this._checkbox.setAttribute("checked", true);
+    } else {
+      this._checkbox.removeAttribute("checked");
+    }
+  },
+
+  /**
    * Sets the contents displayed in this item's view.
    *
    * @param string | nsIDOMNode aContents
    *        The string or node displayed in the container.
    */
   set contents(aContents) {
     // If this item's view contents are a string, then create a label to hold
     // the text displayed in this breadcrumb.
@@ -650,10 +706,11 @@ SideMenuItem.prototype = {
     this._target.appendChild(aContents);
   },
 
   window: null,
   document: null,
   ownerView: null,
   _target: null,
   _container: null,
+  _checkbox: null,
   _arrow: null
 };
--- a/browser/devtools/shared/widgets/ViewHelpers.jsm
+++ b/browser/devtools/shared/widgets/ViewHelpers.jsm
@@ -1607,16 +1607,29 @@ this.WidgetMethods = {
    *         -1 to sort aFirst to a lower index than aSecond
    *          0 to leave aFirst and aSecond unchanged with respect to each other
    *          1 to sort aSecond to a lower index than aFirst
    */
   _currentSortPredicate: function(aFirst, aSecond) {
     return +(aFirst._label.toLowerCase() > aSecond._label.toLowerCase());
   },
 
+  /**
+   * Call a method on this widget named `aMethodName`. Any further arguments are
+   * passed on to the method. Returns the result of the method call.
+   *
+   * @param String aMethodName
+   *        The name of the method you want to call.
+   * @param aArgs
+   *        Optional. Any arguments you want to pass through to the method.
+   */
+  callMethod: function(aMethodName, ...aArgs) {
+    return this._widget[aMethodName].apply(this._widget, aArgs);
+  },
+
   _widget: null,
   _preferredValue: null,
   _cachedCommandDispatcher: null
 };
 
 /**
  * A generator-iterator over all the items in this container.
  */
--- a/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties
@@ -410,16 +410,66 @@ dbgStepInDesc=Executes the current state
 # LOCALIZATION NOTE (dbgStepOutDesc) A very short string used to describe the
 # function of the dbg step out command.
 dbgStepOutDesc=Steps out of the current function and up one level if the function is nested. If in the main body, the script is executed to the end, or to the next breakpoint. The skipped statements are executed, but not stepped through
 
 # LOCALIZATION NOTE (dbgListSourcesDesc) A very short string used to describe the
 # function of the dbg list command.
 dbgListSourcesDesc=List the source URLs loaded in the debugger
 
+# LOCALIZATION NOTE (dbgBlackBoxDesc) A very short string used to describe the
+# function of the 'dbg blackbox' command.
+dbgBlackBoxDesc=Black box sources in the debugger
+
+# LOCALIZATION NOTE (dbgBlackBoxSourceDesc) A very short string used to describe the
+# 'source' parameter to the 'dbg blackbox' command.
+dbgBlackBoxSourceDesc=A specific source to black box
+
+# LOCALIZATION NOTE (dbgBlackBoxGlobDesc) A very short string used to describe the
+# 'glob' parameter to the 'dbg blackbox' command.
+dbgBlackBoxGlobDesc=Black box all sources that match this glob (for example: "*.min.js")
+
+# LOCALIZATION NOTE (dbgBlackBoxEmptyDesc) A very short string used to let the
+# user know that no sources were black boxed.
+dbgBlackBoxEmptyDesc=(No sources black boxed)
+
+# LOCALIZATION NOTE (dbgBlackBoxNonEmptyDesc) A very short string used to let the
+# user know which sources were black boxed.
+dbgBlackBoxNonEmptyDesc=The following sources were black boxed:
+
+# LOCALIZATION NOTE (dbgBlackBoxErrorDesc) A very short string used to let the
+# user know there was an error black boxing a source (whose url follows this
+# text).
+dbgBlackBoxErrorDesc=Error black boxing:
+
+# LOCALIZATION NOTE (dbgUnBlackBoxDesc) A very short string used to describe the
+# function of the 'dbg unblackbox' command.
+dbgUnBlackBoxDesc=Stop black boxing sources in the debugger
+
+# LOCALIZATION NOTE (dbgUnBlackBoxSourceDesc) A very short string used to describe the
+# 'source' parameter to the 'dbg unblackbox' command.
+dbgUnBlackBoxSourceDesc=A specific source to stop black boxing
+
+# LOCALIZATION NOTE (dbgUnBlackBoxGlobDesc) A very short string used to describe the
+# 'glob' parameter to the 'dbg blackbox' command.
+dbgUnBlackBoxGlobDesc=Stop black boxing all sources that match this glob (for example: "*.min.js")
+
+# LOCALIZATION NOTE (dbgUnBlackBoxEmptyDesc) A very short string used to let the
+# user know that we did not stop black boxing any sources.
+dbgUnBlackBoxEmptyDesc=(Did not stop black boxing any sources)
+
+# LOCALIZATION NOTE (dbgUnBlackBoxNonEmptyDesc) A very short string used to let the
+# user know which sources we stopped black boxing.
+dbgUnBlackBoxNonEmptyDesc=Stopped black boxing the following sources:
+
+# LOCALIZATION NOTE (dbgUnBlackBoxErrorDesc) A very short string used to let the
+# user know there was an error black boxing a source (whose url follows this
+# text).
+dbgUnBlackBoxErrorDesc=Error stopping black boxing:
+
 # LOCALIZATION NOTE (consolecloseDesc) A very short description of the
 # 'console close' command. This string is designed to be shown in a menu
 # alongside the command name, which is why it should be as short as possible.
 consolecloseDesc=Close the console
 
 # LOCALIZATION NOTE (consoleopenDesc) A very short description of the
 # 'console open' command. This string is designed to be shown in a menu
 # alongside the command name, which is why it should be as short as possible.
--- a/browser/themes/linux/devtools/debugger.css
+++ b/browser/themes/linux/devtools/debugger.css
@@ -45,16 +45,20 @@
   height: 24px;
   border: 0;
 }
 
 .side-menu-widget-item-checkbox[checked] > .checkbox-check {
   background-position: 0 0;
 }
 
+.side-menu-widget-item-checkbox:not([checked]) ~ .side-menu-widget-item-contents > .dbg-breakpoint {
+  display: none;
+}
+
 /* ListWidget items */
 
 .list-widget-item {
   padding: 2px;
 }
 
 .list-widget-item:not(.selected):not(.empty):hover {
   background: linear-gradient(rgba(255,255,255,0.9), rgba(255,255,255,0.85)), Highlight;
--- a/browser/themes/osx/devtools/debugger.css
+++ b/browser/themes/osx/devtools/debugger.css
@@ -47,16 +47,20 @@
   height: 24px;
   border: 0;
 }
 
 .side-menu-widget-item-checkbox[checked] > .checkbox-check {
   background-position: 0 0;
 }
 
+.side-menu-widget-item-checkbox:not([checked]) ~ .side-menu-widget-item-contents > .dbg-breakpoint {
+  display: none;
+}
+
 /* ListWidget items */
 
 .list-widget-item {
   padding: 2px;
 }
 
 .list-widget-item:not(.selected):not(.empty):hover {
   background: linear-gradient(rgba(255,255,255,0.9), rgba(255,255,255,0.85)), Highlight;
--- a/browser/themes/windows/devtools/debugger.css
+++ b/browser/themes/windows/devtools/debugger.css
@@ -45,16 +45,20 @@
   height: 24px;
   border: 0;
 }
 
 .side-menu-widget-item-checkbox[checked] > .checkbox-check {
   background-position: 0 0;
 }
 
+.side-menu-widget-item-checkbox:not([checked]) ~ .side-menu-widget-item-contents > .dbg-breakpoint {
+  display: none;
+}
+
 /* ListWidget items */
 
 .list-widget-item {
   padding: 2px;
 }
 
 .list-widget-item:not(.selected):not(.empty):hover {
   background: linear-gradient(rgba(255,255,255,0.9), rgba(255,255,255,0.85)), Highlight;
--- a/testing/marionette/components/moz.build
+++ b/testing/marionette/components/moz.build
@@ -1,10 +1,13 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
+EXTRA_COMPONENTS += [
+    'MarionetteComponents.manifest'
+]
+
 EXTRA_PP_COMPONENTS += [
-    'MarionetteComponents.manifest',
-    'marionettecomponent.js',
+    'marionettecomponent.js'
 ]
--- a/toolkit/devtools/client/dbg-client.jsm
+++ b/toolkit/devtools/client/dbg-client.jsm
@@ -1752,33 +1752,38 @@ LongStringClient.prototype = {
 function SourceClient(aClient, aForm) {
   this._form = aForm;
   this._isBlackBoxed = aForm.isBlackBoxed;
   this._client = aClient;
 }
 
 SourceClient.prototype = {
   get _transport() this._client._transport,
+  get _activeThread() this._client.activeThread,
   get isBlackBoxed() this._isBlackBoxed,
   get actor() this._form.actor,
   get request() this._client.request,
+  get url() this._form.url,
 
   /**
    * Black box this SourceClient's source.
    *
    * @param aCallback Function
    *        The callback function called when we receive the response from the server.
    */
   blackBox: DebuggerClient.requester({
     type: "blackbox"
   }, {
     telemetry: "BLACKBOX",
     after: function (aResponse) {
       if (!aResponse.error) {
         this._isBlackBoxed = true;
+        if (this._activeThread) {
+          this._activeThread.notify("blackboxchange", this);
+        }
       }
       return aResponse;
     }
   }),
 
   /**
    * Un-black box this SourceClient's source.
    *
@@ -1787,16 +1792,19 @@ SourceClient.prototype = {
    */
   unblackBox: DebuggerClient.requester({
     type: "unblackbox"
   }, {
     telemetry: "UNBLACKBOX",
     after: function (aResponse) {
       if (!aResponse.error) {
         this._isBlackBoxed = false;
+        if (this._activeThread) {
+          this._activeThread.notify("blackboxchange", this);
+        }
       }
       return aResponse;
     }
   }),
 
   /**
    * Get a long string grip for this SourceClient's source.
    */
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -591,18 +591,18 @@ var NodeListActor = exports.NodeListActo
       this.walker.ensurePathToRoot(item, newParents);
     }
     return {
       nodes: items,
       newParents: [node for (node of newParents)]
     }
   }, {
     request: {
-      start: Arg(0, "number", { optional: true }),
-      end: Arg(1, "number", { optional: true })
+      start: Arg(0, "nullable:number"),
+      end: Arg(1, "nullable:number")
     },
     response: RetVal("disconnectedNodeArray")
   }),
 
   release: method(function() {}, { release: true })
 });
 
 /**
@@ -659,17 +659,17 @@ let nodeArrayMethod = {
 };
 
 let traversalMethod = {
   request: {
     node: Arg(0, "domnode"),
     whatToShow: Option(1)
   },
   response: {
-    node: RetVal("domnode", {optional: true})
+    node: RetVal("nullable:domnode")
   }
 }
 
 /**
  * We need to know when a document is navigating away so that we can kill
  * the nodes underneath it.  We also need to know when a document is
  * navigated to so that we can send a mutation event for the iframe node.
  *
@@ -834,32 +834,32 @@ var WalkerActor = protocol.ActorClass({
    * @param NodeActor node
    *        The node whose document is needed, or null to
    *        return the root.
    */
   document: method(function(node) {
     let doc = node ? nodeDocument(node.rawNode) : this.rootDoc;
     return this._ref(doc);
   }, {
-    request: { node: Arg(0, "domnode", {optional: true}) },
+    request: { node: Arg(0, "nullable:domnode") },
     response: { node: RetVal("domnode") },
   }),
 
   /**
    * Return the documentElement for the document containing the
    * given node.
    * @param NodeActor node
    *        The node whose documentElement is requested, or null
    *        to use the root document.
    */
   documentElement: method(function(node) {
     let elt = node ? nodeDocument(node.rawNode).documentElement : this.rootDoc.documentElement;
     return this._ref(elt);
   }, {
-    request: { node: Arg(0, "domnode", {optional: true}) },
+    request: { node: Arg(0, "nullable:domnode") },
     response: { node: RetVal("domnode") },
   }),
 
   /**
    * Return all parents of the given node, ordered from immediate parent
    * to root.
    * @param NodeActor node
    *    The node whose parents are requested.
@@ -1352,17 +1352,17 @@ var WalkerActor = protocol.ActorClass({
       for (let locked of this._activePseudoClassLocks) {
         DOMUtils.clearPseudoClassLocks(locked.rawNode);
         this._activePseudoClassLocks.delete(locked);
         this._queuePseudoClassMutation(locked);
       }
     }
   }, {
     request: {
-      node: Arg(0, "domnode", { optional: true }),
+      node: Arg(0, "nullable:domnode")
     },
     response: {}
   }),
 
   /**
    * Get a node's innerHTML property.
    */
   innerHTML: method(function(node) {
@@ -1407,30 +1407,30 @@ var WalkerActor = protocol.ActorClass({
       // Mutation events will take care of the rest.
     }
     return nextSibling;
   }, {
     request: {
       node: Arg(0, "domnode")
     },
     response: {
-      nextSibling: RetVal("domnode", { optional: true })
+      nextSibling: RetVal("nullable:domnode")
     }
   }),
 
   /**
    * Insert a node into the DOM.
    */
   insertBefore: method(function(node, parent, sibling) {
     parent.rawNode.insertBefore(node.rawNode, sibling ? sibling.rawNode : null);
   }, {
     request: {
       node: Arg(0, "domnode"),
       parent: Arg(1, "domnode"),
-      sibling: Arg(2, "domnode", { optional: true })
+      sibling: Arg(2, "nullable:domnode")
     },
     response: {}
   }),
 
   /**
    * Get any pending mutation records.  Must be called by the client after
    * the `new-mutations` notification is received.  Returns an array of
    * mutation records.
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -3184,50 +3184,71 @@ ThreadSources.prototype = {
    * |aScript| must have a non-null sourceMapURL.
    */
   sourceMap: function TS_sourceMap(aScript) {
     if (aScript.url in this._sourceMapsByGeneratedSource) {
       return this._sourceMapsByGeneratedSource[aScript.url];
     }
     dbg_assert(aScript.sourceMapURL, "Script should have a sourceMapURL");
     let sourceMapURL = this._normalize(aScript.sourceMapURL, aScript.url);
-    let map = this._fetchSourceMap(sourceMapURL)
+    let map = this._fetchSourceMap(sourceMapURL, aScript.url)
       .then((aSourceMap) => {
         for (let s of aSourceMap.sources) {
           this._generatedUrlsByOriginalUrl[s] = aScript.url;
           this._sourceMapsByOriginalSource[s] = resolve(aSourceMap);
         }
         return aSourceMap;
       });
     this._sourceMapsByGeneratedSource[aScript.url] = map;
     return map;
   },
 
   /**
    * Return a promise of a SourceMapConsumer for the source map located at
    * |aAbsSourceMapURL|, which must be absolute. If there is already such a
    * promise extant, return it.
+   *
+   * @param string aAbsSourceMapURL
+   *        The source map URL, in absolute form, not relative.
+   * @param string aScriptURL
+   *        When the source map URL is a data URI, there is no sourceRoot on the
+   *        source map, and the source map's sources are relative, we resolve
+   *        them from aScriptURL.
    */
-  _fetchSourceMap: function TS__fetchSourceMap(aAbsSourceMapURL) {
+  _fetchSourceMap: function TS__fetchSourceMap(aAbsSourceMapURL, aScriptURL) {
     if (aAbsSourceMapURL in this._sourceMaps) {
       return this._sourceMaps[aAbsSourceMapURL];
-    } else {
-      let promise = fetch(aAbsSourceMapURL).then(rawSourceMap => {
-        let map = new SourceMapConsumer(rawSourceMap);
-        let base = aAbsSourceMapURL.replace(/\/[^\/]+$/, '/');
-        if (base.indexOf("data:") !== 0) {
-          map.sourceRoot = map.sourceRoot
-            ? this._normalize(map.sourceRoot, base)
-            : base;
-        }
-        return map;
-      });
-      this._sourceMaps[aAbsSourceMapURL] = promise;
-      return promise;
     }
+
+    let promise = fetch(aAbsSourceMapURL).then(rawSourceMap => {
+      let map = new SourceMapConsumer(rawSourceMap);
+      this._setSourceMapRoot(map, aAbsSourceMapURL, aScriptURL);
+      return map;
+    });
+    this._sourceMaps[aAbsSourceMapURL] = promise;
+    return promise;
+  },
+
+  /**
+   * Sets the source map's sourceRoot to be relative to the source map url.
+   */
+  _setSourceMapRoot: function TS__setSourceMapRoot(aSourceMap, aAbsSourceMapURL,
+                                                   aScriptURL) {
+    const base = this._dirname(
+      aAbsSourceMapURL.indexOf("data:") === 0
+        ? aScriptURL
+        : aAbsSourceMapURL);
+    aSourceMap.sourceRoot = aSourceMap.sourceRoot
+      ? this._normalize(aSourceMap.sourceRoot, base)
+      : base;
+  },
+
+  _dirname: function TS__dirname(aPath) {
+    return Services.io.newURI(
+      ".", null, Services.io.newURI(aPath, null, null)).spec;
   },
 
   /**
    * Returns a promise of the location in the original source if the source is
    * source mapped, otherwise a promise of the same location.
    */
   getOriginalLocation:
   function TS_getOriginalLocation(aSourceUrl, aLine, aColumn) {
--- a/toolkit/devtools/server/protocol.js
+++ b/toolkit/devtools/server/protocol.js
@@ -76,16 +76,18 @@ types.getType = function(type) {
   // New type, see if it's a collection/lifetime type:
   let sep = type.indexOf(":");
   if (sep >= 0) {
     let collection = type.substring(0, sep);
     let subtype = types.getType(type.substring(sep + 1));
 
     if (collection === "array") {
       return types.addArrayType(subtype);
+    } else if (collection === "nullable") {
+      return types.addNullableType(subtype);
     }
 
     if (registeredLifetimes.has(collection)) {
       return types.addLifetimeType(collection, subtype);
     }
 
     throw Error("Unknown collection type: " + collection);
   }
@@ -101,16 +103,27 @@ types.getType = function(type) {
     require("devtools/server/actors/string");
     return registeredTypes.get("longstring");
   }
 
   throw Error("Unknown type: " + type);
 }
 
 /**
+ * Don't allow undefined when writing primitive types to packets.  If
+ * you want to allow undefined, use a nullable type.
+ */
+function identityWrite(v) {
+  if (v === undefined) {
+    throw Error("undefined passed where a value is required");
+  }
+  return v;
+}
+
+/**
  * Add a type to the type system.
  *
  * When registering a type, you can provide `read` and `write` methods.
  *
  * The `read` method will be passed a JS object value from the JSON
  * packet and must return a native representation.  The `write` method will
  * be passed a native representation and should provide a JSONable value.
  *
@@ -131,18 +144,18 @@ types.getType = function(type) {
 types.addType = function(name, typeObject={}, options={}) {
   if (registeredTypes.has(name)) {
     throw Error("Type '" + name + "' already exists.");
   }
 
   let type = object.merge({
     name: name,
     primitive: !(typeObject.read || typeObject.write),
-    read: v => v,
-    write: v => v
+    read: identityWrite,
+    write: identityWrite
   }, typeObject);
 
   registeredTypes.set(name, type);
 
   if (!options.thawed) {
     Object.freeze(type);
   }
 
@@ -236,22 +249,23 @@ types.addActorType = function(name) {
       // find the actor registered with this actorID.
       if (ctx instanceof Actor) {
         return ctx.conn.getActor(v);
       }
 
       // Reading a response on the client side, check for an
       // existing front on the connection, and create the front
       // if it isn't found.
-      let front = ctx.conn.getActor(v.actor);
+      let actorID = typeof(v) === "string" ? v : v.actor;
+      let front = ctx.conn.getActor(actorID);
       if (front) {
         front.form(v, detail, ctx);
       } else {
         front = new type.frontClass(ctx.conn, v, detail, ctx)
-        front.actorID = v.actor;
+        front.actorID = actorID;
         ctx.marshallPool().manage(front);
       }
       return front;
     },
     write: (v, ctx, detail) => {
       // If returning a response from the server side, make sure
       // the actor is added to a parent object and return its form.
       if (v instanceof Actor) {
@@ -267,16 +281,34 @@ types.addActorType = function(name) {
   }, {
     // We usually freeze types, but actor types are updated when clients are
     // created, so don't freeze yet.
     thawed: true
   });
   return type;
 }
 
+types.addNullableType = function(subtype) {
+  subtype = types.getType(subtype);
+  return types.addType("nullable:" + subtype.name, {
+    read: (value, ctx) => {
+      if (value == null) {
+        return value;
+      }
+      return subtype.read(value, ctx);
+    },
+    write: (value, ctx) => {
+      if (value == null) {
+        return value;
+      }
+      return subtype.write(value, ctx);
+    }
+  });
+}
+
 /**
  * Register an actor detail type.  This is just like an actor type, but
  * will pass a detail hint to the actor's form method during serialization/
  * deserialization.
  *
  * This is called by getType() when passed an 'actorType#detail' string.
  *
  * @param string name
@@ -359,42 +391,29 @@ types.JSON = types.addType("json");
 
 /**
  * Placeholder for simple arguments.
  *
  * @param number index
  *    The argument index to place at this position.
  * @param type type
  *    The argument should be marshalled as this type.
- * @param object options
- *    Argument options:
- *      optional: true if the argument can be undefined or null.
  * @constructor
  */
 let Arg = Class({
-  initialize: function(index, type, options={}) {
+  initialize: function(index, type) {
     this.index = index;
     this.type = types.getType(type);
-    this.optional = !!options.optional;
   },
 
   write: function(arg, ctx) {
-    if (arg === undefined || arg === null) {
-      if (!this.optional) throw Error("Required argument " + this.name + " not specified.");
-      return undefined;
-    }
     return this.type.write(arg, ctx);
   },
 
   read: function(v, ctx, outArgs) {
-    if (v === undefined || v === null) {
-      if (!this.optional) throw Error("Required argument " + this.name + " not specified.");
-      outArgs[this.index] = v;
-      return;
-    }
     outArgs[this.index] = this.type.read(v, ctx);
   }
 });
 exports.Arg = Arg;
 
 /**
  * Placeholder for an options argument value that should be hoisted
  * into the packet.
@@ -441,44 +460,28 @@ let Option = Class({
 
 exports.Option = Option;
 
 /**
  * Placeholder for return values in a response template.
  *
  * @param type type
  *    The return value should be marshalled as this type.
- * @param object options
- *    Argument options:
- *      optional: true if the argument can be undefined or null.
  */
 let RetVal = Class({
-  initialize: function(type, options={}) {
+  initialize: function(type) {
     this.type = types.getType(type);
-    this.optional = !!options.optional;
   },
 
   write: function(v, ctx) {
-    if (v !== undefined && v != null) {
-      return this.type.write(v, ctx);
-    }
-    if (!this.optional) {
-      throw Error("Return value not specified.");
-    }
-    return v;
+    return this.type.write(v, ctx);
   },
 
   read: function(v, ctx) {
-    if (v !== undefined && v != null) {
-      return this.type.read(v, ctx);
-    }
-    if (!this.optional) {
-      throw Error("Return value not specified.");
-    }
-    return v;
+    return this.type.read(v, ctx);
   }
 });
 
 exports.RetVal = RetVal;
 
 /* Template handling functions */
 
 /**
--- a/toolkit/devtools/server/tests/unit/test_protocol_children.js
+++ b/toolkit/devtools/server/tests/unit/test_protocol_children.js
@@ -38,16 +38,19 @@ let ChildActor = protocol.ActorClass({
   },
 
   destroy: function() {
     protocol.Actor.prototype.destroy.call(this);
     this.destroyed = true;
   },
 
   form: function(detail) {
+    if (detail === "actorid") {
+      return this.actorID;
+    }
     return {
       actor: this.actorID,
       childID: this.childID,
       detail: detail
     };
   },
 
   echo: method(function(str) {
@@ -67,16 +70,24 @@ let ChildActor = protocol.ActorClass({
 
   getDetail2: method(function() {
     return this;
   }, {
     // This also exercises return-value-as-packet.
     response: RetVal("childActor#detail2"),
   }),
 
+  getIDDetail: method(function() {
+    return this;
+  }, {
+    response: {
+      idDetail: RetVal("childActor#actorid")
+    }
+  }),
+
   getSibling: method(function(id) {
     return this.parent().getChild(id);
   }, {
     request: { id: Arg(0) },
     response: { sibling: RetVal("childActor") }
   }),
 
   emitEvents: method(function() {
@@ -122,17 +133,20 @@ let ChildFront = protocol.FrontClass(Chi
     this.destroyed = true;
     protocol.Front.prototype.destroy.call(this);
   },
 
   marshallPool: function() { return this.parent() },
 
   toString: function() "[child front " + this.childID + "]",
 
-  form: function(form) {
+  form: function(form, detail) {
+    if (detail === "actorid") {
+      return;
+    }
     this.childID = form.childID;
     this.detail = form.detail;
   },
 
   onEvent1: preEvent("event1", function(a, b, c) {
     this.event1arg3 = c;
   }),
 });
@@ -300,16 +314,22 @@ function run_test()
     }).then(() => {
       return childFront.getDetail2();
     }).then(ret => {
       trace.expectSend({"type":"getDetail2","to":"<actorid>"});
       trace.expectReceive({"actor":"<actorid>","childID":"child1","detail":"detail2","from":"<actorid>"});
       do_check_true(ret === childFront);
       do_check_eq(childFront.detail, "detail2");
     }).then(() => {
+      return childFront.getIDDetail();
+    }).then(ret => {
+      trace.expectSend({"type":"getIDDetail","to":"<actorid>"});
+      trace.expectReceive({"idDetail": childFront.actorID,"from":"<actorid>"});
+      do_check_true(ret === childFront);
+    }).then(() => {
       return childFront.getSibling("siblingID");
     }).then(ret => {
       trace.expectSend({"type":"getSibling","id":"siblingID","to":"<actorid>"});
       trace.expectReceive({"sibling":{"actor":"<actorid>","childID":"siblingID"},"from":"<actorid>"});
 
       expectRootChildren(2);
     }).then(ret => {
       return rootFront.getTemporaryChild("temp1").then(temp1 => {
--- a/toolkit/devtools/server/tests/unit/test_protocol_simple.js
+++ b/toolkit/devtools/server/tests/unit/test_protocol_simple.js
@@ -76,17 +76,17 @@ let RootActor = protocol.ActorClass({
     response: RetVal()
   }),
 
   optionalArgs: method(function(a, b=200) {
     return b;
   }, {
     request: {
       a: Arg(0),
-      b: Arg(1, "number", { optional: true })
+      b: Arg(1, "nullable:number")
     },
     response: {
       value: RetVal("number")
     },
   }),
 
   arrayArgs: method(function(a) {
     return a;
@@ -183,16 +183,21 @@ function run_test()
       do_check_eq(ret, 1);
     }).then(() => {
       return rootClient.promiseReturn();
     }).then(ret => {
       trace.expectSend({"type":"promiseReturn","to":"<actorid>"});
       trace.expectReceive({"value":1,"from":"<actorid>"});
       do_check_eq(ret, 1);
     }).then(() => {
+      // Missing argument should throw an exception
+      check_except(() => {
+        rootClient.simpleArgs(5);
+      });
+
       return rootClient.simpleArgs(5, 10)
     }).then(ret => {
       trace.expectSend({"type":"simpleArgs","firstArg":5,"secondArg":10,"to":"<actorid>"});
       trace.expectReceive({"firstResponse":6,"secondResponse":11,"from":"<actorid>"});
       do_check_eq(ret.firstResponse, 6);
       do_check_eq(ret.secondResponse, 11);
     }).then(() => {
       return rootClient.nestedArgs(1, 2, 3);
--- a/toolkit/devtools/server/tests/unit/test_sourcemaps-06.js
+++ b/toolkit/devtools/server/tests/unit/test_sourcemaps-06.js
@@ -46,19 +46,19 @@ function test_source_content()
   });
 
   let node = new SourceNode(null, null, null, [
     new SourceNode(1, 0, "a.js", "function a() { return 'a'; }\n"),
     new SourceNode(1, 0, "b.js", "function b() { return 'b'; }\n"),
     new SourceNode(1, 0, "c.js", "function c() { return 'c'; }\n"),
   ]);
 
-  node.setSourceContent("a.js", "content for a.js");
-  node.setSourceContent("b.js", "content for b.js");
-  node.setSourceContent("c.js", "content for c.js");
+  node.setSourceContent("a.js", "content for http://example.com/www/js/a.js");
+  node.setSourceContent("b.js", "content for http://example.com/www/js/b.js");
+  node.setSourceContent("c.js", "content for http://example.com/www/js/c.js");
 
   let { code, map } = node.toStringWithSourceMap({
     file: "abc.js"
   });
 
   code += "//# sourceMappingURL=data:text/json;base64," + btoa(map.toString());
 
   Components.utils.evalInSandbox(code, gDebuggee, "1.8",
--- a/toolkit/devtools/server/tests/unit/test_sourcemaps-09.js
+++ b/toolkit/devtools/server/tests/unit/test_sourcemaps-09.js
@@ -29,30 +29,30 @@ function test_minified()
 {
   let newSourceFired = false;
 
   gClient.addOneTimeListener("newSource", function _onNewSource(aEvent, aPacket) {
     do_check_eq(aEvent, "newSource");
     do_check_eq(aPacket.type, "newSource");
     do_check_true(!!aPacket.source);
 
-    do_check_eq(aPacket.source.url, "foo.js",
+    do_check_eq(aPacket.source.url, "http://example.com/foo.js",
                 "The new source should be foo.js");
     do_check_eq(aPacket.source.url.indexOf("foo.min.js"), -1,
                 "The new source should not be the minified file");
 
     newSourceFired = true;
   });
 
   gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
     do_check_eq(aEvent, "paused");
     do_check_eq(aPacket.why.type, "debuggerStatement");
 
     const location = {
-      url: "foo.js",
+      url: "http://example.com/foo.js",
       line: 5
     };
 
     gThreadClient.setBreakpoint(location, function (aResponse, bpClient) {
       do_check_true(!aResponse.error);
       testHitBreakpoint();
     });
   });