Merge fx-team to m-c a=merge
authorWes Kocher <wkocher@mozilla.com>
Thu, 11 Dec 2014 16:28:06 -0800
changeset 219150 32a2c5bd2f6833fcf300333e801924528f583def
parent 219149 d92db71d4e67181af10e3b97f91022be1e3084c6 (current diff)
parent 219142 0d35326dfbae2d2dc62ebcd99d8696d8fedb9bbf (diff)
child 219189 190b2d00739f0f061622c169285f181560e6f30a
push id10368
push userkwierso@gmail.com
push dateFri, 12 Dec 2014 01:38:39 +0000
treeherderfx-team@5288b15d22de [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone37.0a1
Merge fx-team to m-c a=merge
dom/media/tests/mochitest/test_peerConnection_bug835370.html
toolkit/themes/shared/in-content/check.png
toolkit/themes/shared/in-content/check@2x.png
--- a/b2g/chrome/content/desktop.js
+++ b/b2g/chrome/content/desktop.js
@@ -51,18 +51,18 @@ function setupButtons() {
     GlobalSimulatorScreen.flipScreen();
     rotateButton.classList.remove('active');
   });
 }
 
 function checkDebuggerPort() {
   // XXX: To be removed once bug 942756 lands.
   // We are hacking 'unix-domain-socket' pref by setting a tcp port (number).
-  // DebuggerServer.openListener detects that it isn't a file path (string),
-  // and starts listening on the tcp port given here as command line argument.
+  // SocketListener.open detects that it isn't a file path (string), and starts
+  // listening on the tcp port given here as command line argument.
 
   // Get the command line arguments that were passed to the b2g client
   let args;
   try {
     let service = Cc["@mozilla.org/commandlinehandler/general-startup;1?type=b2gcmds"].getService(Ci.nsISupports);
     args = service.wrappedJSObject.cmdLine;
   } catch(e) {}
 
--- a/b2g/chrome/content/devtools/debugger.js
+++ b/b2g/chrome/content/devtools/debugger.js
@@ -12,20 +12,16 @@ XPCOMUtils.defineLazyGetter(this, "Debug
 });
 
 XPCOMUtils.defineLazyGetter(this, "devtools", function() {
   const { devtools } =
     Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
   return devtools;
 });
 
-XPCOMUtils.defineLazyGetter(this, "discovery", function() {
-  return devtools.require("devtools/toolkit/discovery/discovery");
-});
-
 XPCOMUtils.defineLazyGetter(this, "B2GTabList", function() {
   const { B2GTabList } =
     devtools.require("resource://gre/modules/DebuggerActors.js");
   return B2GTabList;
 });
 
 let RemoteDebugger = {
   _promptDone: false,
@@ -140,18 +136,20 @@ let USBRemoteDebugger = {
     RemoteDebugger.initServer();
 
     let portOrPath =
       Services.prefs.getCharPref("devtools.debugger.unix-domain-socket") ||
       "/data/local/debugger-socket";
 
     try {
       debug("Starting USB debugger on " + portOrPath);
-      this._listener = DebuggerServer.openListener(portOrPath);
+      this._listener = DebuggerServer.createListener();
+      this._listener.portOrPath = portOrPath;
       this._listener.allowConnection = RemoteDebugger.prompt;
+      this._listener.open();
       // Temporary event, until bug 942756 lands and offers a way to know
       // when the server is up and running.
       Services.obs.notifyObservers(null, "debugger-server-started", null);
     } catch (e) {
       debug("Unable to start USB debugger server: " + e);
     }
   },
 
@@ -176,33 +174,35 @@ let WiFiRemoteDebugger = {
     if (this._listener) {
       return;
     }
 
     RemoteDebugger.initServer();
 
     try {
       debug("Starting WiFi debugger");
-      this._listener = DebuggerServer.openListener(-1);
+      this._listener = DebuggerServer.createListener();
+      this._listener.portOrPath = -1 /* any available port */;
       this._listener.allowConnection = RemoteDebugger.prompt;
+      this._listener.discoverable = true;
+      this._listener.encryption = true;
+      this._listener.open();
       let port = this._listener.port;
       debug("Started WiFi debugger on " + port);
-      discovery.addService("devtools", { port: port });
     } catch (e) {
       debug("Unable to start WiFi debugger server: " + e);
     }
   },
 
   stop: function() {
     if (!this._listener) {
       return;
     }
 
     try {
-      discovery.removeService("devtools");
       this._listener.close();
       this._listener = null;
     } catch (e) {
       debug("Unable to stop WiFi debugger server: " + e);
     }
   }
 
 };
--- a/browser/base/content/browser-social.js
+++ b/browser/base/content/browser-social.js
@@ -547,16 +547,17 @@ SocialShare = {
     for (let provider of providers) {
       let button = document.createElement("toolbarbutton");
       button.setAttribute("class", "toolbarbutton share-provider-button");
       button.setAttribute("type", "radio");
       button.setAttribute("group", "share-providers");
       button.setAttribute("image", provider.iconURL);
       button.setAttribute("tooltip", "share-button-tooltip");
       button.setAttribute("origin", provider.origin);
+      button.setAttribute("label", provider.name);
       button.setAttribute("oncommand", "SocialShare.sharePage(this.getAttribute('origin'));");
       if (provider == selectedProvider) {
         this.defaultButton = button;
       }
       hbox.insertBefore(button, addButton);
     }
     if (!this.defaultButton) {
       this.defaultButton = addButton;
--- a/browser/base/content/socialchat.xml
+++ b/browser/base/content/socialchat.xml
@@ -48,16 +48,24 @@
         }
         this.addEventListener("DOMContentLoaded", function DOMContentLoaded(event) {
           if (event.target != this.contentDocument)
             return;
           this.removeEventListener("DOMContentLoaded", DOMContentLoaded, true);
           this.isActive = !this.minimized;
           this._deferredChatLoaded.resolve(this);
         }, true);
+
+        // load content.js to support webrtc, fullscreen, etc.
+        this.addEventListener("load", function loaded(event) {
+          this.removeEventListener("load", loaded, true);
+          let mm = this.content.messageManager;
+          mm.loadFrameScript("chrome://browser/content/content.js", true);
+        }, true);
+
         if (this.src)
           this.setAttribute("src", this.src);
       ]]></constructor>
 
       <field name="_deferredChatLoaded" readonly="true">
         Promise.defer();
       </field>
 
--- a/browser/base/content/test/chat/browser_chatwindow.js
+++ b/browser/base/content/test/chat/browser_chatwindow.js
@@ -1,16 +1,40 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 Components.utils.import("resource://gre/modules/Promise.jsm", this);
 
 let chatbar = document.getElementById("pinnedchats");
 
+function waitForCondition(condition, errorMsg) {
+  let deferred = Promise.defer();
+  var tries = 0;
+  var interval = setInterval(function() {
+    if (tries >= 30) {
+      ok(false, errorMsg);
+      moveOn();
+    }
+    var conditionPassed;
+    try {
+      conditionPassed = condition();
+    } catch (e) {
+      ok(false, e + "\n" + e.stack);
+      conditionPassed = false;
+    }
+    if (conditionPassed) {
+      moveOn();
+    }
+    tries++;
+  }, 100);
+  var moveOn = function() { clearInterval(interval); deferred.resolve(); };
+  return deferred.promise;
+}
+
 add_chat_task(function* testOpenCloseChat() {
   let chatbox = yield promiseOpenChat("http://example.com");
   Assert.strictEqual(chatbox, chatbar.selectedChat);
   // we requested a "normal" chat, so shouldn't be minimized
   Assert.ok(!chatbox.minimized, "chat is not minimized");
   Assert.equal(chatbar.childNodes.length, 1, "should be 1 chat open");
 
 
@@ -94,21 +118,25 @@ add_chat_task(function* testSecondTopLev
 // Test that chats are created in the correct window.
 add_chat_task(function* testChatWindowChooser() {
   let chat = yield promiseOpenChat("http://example.com");
   Assert.equal(numChatsInWindow(window), 1, "first window has the chat");
   // create a second window - this will be the "most recent" and will
   // therefore be the window that hosts the new chat (see bug 835111)
   let secondWindow = OpenBrowserWindow();
   yield promiseOneEvent(secondWindow, "load");
+  Assert.equal(secondWindow, Chat.findChromeWindowForChats(null), "Second window is the preferred chat window");
   Assert.equal(numChatsInWindow(secondWindow), 0, "second window starts with no chats");
   yield promiseOpenChat("http://example.com#2");
   Assert.equal(numChatsInWindow(secondWindow), 1, "second window now has chats");
   Assert.equal(numChatsInWindow(window), 1, "first window still has 1 chat");
   chat.close();
+
+  // a bit heavy handed, but handy fixing bug 1090633
+  yield waitForCondition(function () !chat.parentNode, "chat has been detached");
   Assert.equal(numChatsInWindow(window), 0, "first window now has no chats");
   // now open another chat - it should still open in the second.
   yield promiseOpenChat("http://example.com#3");
   Assert.equal(numChatsInWindow(window), 0, "first window still has no chats");
   Assert.equal(numChatsInWindow(secondWindow), 2, "second window has both chats");
 
   // focus the first window, and open yet another chat - it
   // should open in the first window.
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -955,17 +955,18 @@
   <binding id="browser-search-autocomplete-result-popup" extends="chrome://browser/content/urlbarBindings.xml#browser-autocomplete-result-popup">
     <resources>
       <stylesheet src="chrome://browser/skin/searchbar.css"/>
     </resources>
     <content ignorekeys="true" level="top" consumeoutsideclicks="false">
       <xul:hbox xbl:inherits="collapsed=showonlysettings" anonid="searchbar-engine"
                 class="search-panel-header search-panel-current-engine">
         <xul:image class="searchbar-engine-image" xbl:inherits="src"/>
-        <xul:label anonid="searchbar-engine-name" flex="1" crop="end"/>
+        <xul:label anonid="searchbar-engine-name" flex="1" crop="end"
+                   role="presentation"/>
       </xul:hbox>
       <xul:tree anonid="tree" flex="1"
                 class="autocomplete-tree plain search-panel-tree"
                 hidecolumnpicker="true" seltype="single">
         <xul:treecols anonid="treecols">
           <xul:treecol id="treecolAutoCompleteValue" class="autocomplete-treecol" flex="1" overflow="true"/>
         </xul:treecols>
         <xul:treechildren class="autocomplete-treebody"/>
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -451,16 +451,30 @@ let MozLoopServiceInternal = {
 
     let credentials;
     if (sessionToken) {
       // true = use a hex key, as required by the server (see bug 1032738).
       credentials = deriveHawkCredentials(sessionToken, "sessionToken",
                                           2 * 32, true);
     }
 
+    if (payloadObj) {
+      // Note: we must copy the object rather than mutate it, to avoid
+      // mutating the values of the object passed in.
+      let newPayloadObj = {};
+      for (let property of Object.getOwnPropertyNames(payloadObj)) {
+        if (typeof payloadObj[property] == "string") {
+          newPayloadObj[property] = CommonUtils.encodeUTF8(payloadObj[property]);
+        } else {
+          newPayloadObj[property] = payloadObj[property];
+        }
+      };
+      payloadObj = newPayloadObj;
+    }
+
     return gHawkClient.request(path, method, credentials, payloadObj).then((result) => {
       this.clearError("network");
       return result;
     }, (error) => {
       if (error.code == 401) {
         this.clearSessionToken(sessionType);
 
         if (sessionType == LOOP_SESSION_TYPE.FXA) {
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -22,16 +22,23 @@ loop.conversationViews = (function(mozL1
   function _getPreferredEmail(contact) {
     // A contact may not contain email addresses, but only a phone number.
     if (!contact.email || contact.email.length === 0) {
       return { value: "" };
     }
     return contact.email.find(e => e.pref) || contact.email[0];
   }
 
+  function _getContactDisplayName(contact) {
+    if (contact.name && contact.name[0]) {
+      return contact.name[0];
+    }
+    return _getPreferredEmail(contact).value;
+  }
+
   /**
    * Displays information about the call
    * Caller avatar, name & conversation creation date
    */
   var CallIdentifierView = React.createClass({displayName: 'CallIdentifierView',
     propTypes: {
       peerIdentifier: React.PropTypes.string,
       showIcons: React.PropTypes.bool.isRequired,
@@ -102,24 +109,17 @@ loop.conversationViews = (function(mozL1
    * via children properties.
    */
   var ConversationDetailView = React.createClass({displayName: 'ConversationDetailView',
     propTypes: {
       contact: React.PropTypes.object
     },
 
     render: function() {
-      var contactName;
-
-      if (this.props.contact.name &&
-          this.props.contact.name[0]) {
-        contactName = this.props.contact.name[0];
-      } else {
-        contactName = _getPreferredEmail(this.props.contact).value;
-      }
+      var contactName = _getContactDisplayName(this.props.contact);
 
       document.title = contactName;
 
       return (
         React.DOM.div({className: "call-window"}, 
           CallIdentifierView({
             peerIdentifier: contactName, 
             showIcons: false}), 
@@ -257,17 +257,20 @@ loop.conversationViews = (function(mozL1
     },
 
     emailLink: function() {
       this.setState({
         emailLinkError: false,
         emailLinkButtonDisabled: true
       });
 
-      this.props.dispatcher.dispatch(new sharedActions.FetchEmailLink());
+      this.props.dispatcher.dispatch(new sharedActions.FetchRoomEmailLink({
+        roomOwner: navigator.mozLoop.userProfile.email,
+        roomName: _getContactDisplayName(this.props.contact)
+      }));
     },
 
     render: function() {
       return (
         React.DOM.div({className: "call-window"}, 
           React.DOM.h2(null, mozL10n.get("generic_failure_title")), 
 
           React.DOM.p({className: "btn-label"}, mozL10n.get("generic_failure_with_reason2")), 
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -22,16 +22,23 @@ loop.conversationViews = (function(mozL1
   function _getPreferredEmail(contact) {
     // A contact may not contain email addresses, but only a phone number.
     if (!contact.email || contact.email.length === 0) {
       return { value: "" };
     }
     return contact.email.find(e => e.pref) || contact.email[0];
   }
 
+  function _getContactDisplayName(contact) {
+    if (contact.name && contact.name[0]) {
+      return contact.name[0];
+    }
+    return _getPreferredEmail(contact).value;
+  }
+
   /**
    * Displays information about the call
    * Caller avatar, name & conversation creation date
    */
   var CallIdentifierView = React.createClass({
     propTypes: {
       peerIdentifier: React.PropTypes.string,
       showIcons: React.PropTypes.bool.isRequired,
@@ -102,24 +109,17 @@ loop.conversationViews = (function(mozL1
    * via children properties.
    */
   var ConversationDetailView = React.createClass({
     propTypes: {
       contact: React.PropTypes.object
     },
 
     render: function() {
-      var contactName;
-
-      if (this.props.contact.name &&
-          this.props.contact.name[0]) {
-        contactName = this.props.contact.name[0];
-      } else {
-        contactName = _getPreferredEmail(this.props.contact).value;
-      }
+      var contactName = _getContactDisplayName(this.props.contact);
 
       document.title = contactName;
 
       return (
         <div className="call-window">
           <CallIdentifierView
             peerIdentifier={contactName}
             showIcons={false} />
@@ -257,17 +257,20 @@ loop.conversationViews = (function(mozL1
     },
 
     emailLink: function() {
       this.setState({
         emailLinkError: false,
         emailLinkButtonDisabled: true
       });
 
-      this.props.dispatcher.dispatch(new sharedActions.FetchEmailLink());
+      this.props.dispatcher.dispatch(new sharedActions.FetchRoomEmailLink({
+        roomOwner: navigator.mozLoop.userProfile.email,
+        roomName: _getContactDisplayName(this.props.contact)
+      }));
     },
 
     render: function() {
       return (
         <div className="call-window">
           <h2>{mozL10n.get("generic_failure_title")}</h2>
 
           <p className="btn-label">{mozL10n.get("generic_failure_with_reason2")}</p>
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -21,16 +21,17 @@
 }
 
 /* desktop version */
 .fx-embedded .conversation-toolbar {
   position: absolute;
   top: 0;
   left: 0;
   right: 0;
+  /* note that .room-invitation-overlay top matches this */
   height: 26px;
 }
 
 /* standalone version */
 .standalone .conversation-toolbar {
   padding: 20px;
   height: 64px;
 }
@@ -746,17 +747,18 @@ html, .fx-embedded, #main,
 
 .fx-embedded .room-conversation .conversation-toolbar .btn-hangup {
   background-image: url("../img/icons-16x16.svg#leave");
 }
 
 .room-invitation-overlay {
   position: absolute;
   background: rgba(0, 0, 0, .6);
-  top: 0;
+  /* This matches .fx-embedded .conversation toolbar height */
+  top: 26px;
   right: 0;
   bottom: 0;
   left: 0;
   text-align: center;
   color: #fff;
   z-index: 1010;
 }
 
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -71,20 +71,22 @@ loop.shared.actions = (function() {
 
     /**
      * Used to signal when the window is being unloaded.
      */
     WindowUnload: Action.define("windowUnload", {
     }),
 
     /**
-     * Fetch a new call url from the server, intended to be sent over email when
+     * Fetch a new room url from the server, intended to be sent over email when
      * a contact can't be reached.
      */
-    FetchEmailLink: Action.define("fetchEmailLink", {
+    FetchRoomEmailLink: Action.define("fetchRoomEmailLink", {
+      roomOwner: String,
+      roomName: String
     }),
 
     /**
      * Used to cancel call setup.
      */
     CancelCall: Action.define("cancelCall", {
     }),
 
--- a/browser/components/loop/content/shared/js/conversationStore.js
+++ b/browser/components/loop/content/shared/js/conversationStore.js
@@ -205,17 +205,17 @@ loop.store = loop.store || {};
         "connectionProgress",
         "connectCall",
         "hangupCall",
         "remotePeerDisconnected",
         "cancelCall",
         "retryCall",
         "mediaConnected",
         "setMute",
-        "fetchEmailLink"
+        "fetchRoomEmailLink"
       ]);
 
       this.setStoreState({
         contact: actionData.contact,
         outgoing: windowType === "outgoing",
         windowId: actionData.windowId,
         callType: actionData.callType,
         callState: CALL_STATES.GATHER,
@@ -318,28 +318,31 @@ loop.store = loop.store || {};
      */
     setMute: function(actionData) {
       var newState = {};
       newState[actionData.type + "Muted"] = !actionData.enabled;
       this.setStoreState(newState);
     },
 
     /**
-     * Fetches a new call URL intended to be sent over email when a contact
+     * Fetches a new room URL intended to be sent over email when a contact
      * can't be reached.
      */
-    fetchEmailLink: function() {
-      // XXX This is an empty string as a conversation identifier. Bug 1015938 implements
-      // a user-set string.
-      this.client.requestCallUrl("", function(err, callUrlData) {
+    fetchRoomEmailLink: function(actionData) {
+      this.mozLoop.rooms.create({
+        roomName: actionData.roomName,
+        roomOwner: actionData.roomOwner,
+        maxSize:   loop.store.MAX_ROOM_CREATION_SIZE,
+        expiresIn: loop.store.DEFAULT_EXPIRES_IN
+      }, function(err, createdRoomData) {
         if (err) {
           this.trigger("error:emailLink");
           return;
         }
-        this.setStoreState({"emailLink": callUrlData.callUrl});
+        this.setStoreState({"emailLink": createdRoomData.roomUrl});
       }.bind(this));
     },
 
     /**
      * Obtains the outgoing call data from the server and handles the
      * result.
      */
     _setupOutgoingCall: function() {
--- a/browser/components/loop/content/shared/js/roomStore.js
+++ b/browser/components/loop/content/shared/js/roomStore.js
@@ -12,16 +12,30 @@ loop.store = loop.store || {};
 
   /**
    * Shared actions.
    * @type {Object}
    */
   var sharedActions = loop.shared.actions;
 
   /**
+   * Maximum size given to createRoom; only 2 is supported (and is
+   * always passed) because that's what the user-experience is currently
+   * designed and tested to handle.
+   * @type {Number}
+   */
+  var MAX_ROOM_CREATION_SIZE = loop.store.MAX_ROOM_CREATION_SIZE = 2;
+
+  /**
+   * The number of hours for which the room will exist - default 8 weeks
+   * @type {Number}
+   */
+  var DEFAULT_EXPIRES_IN = loop.store.DEFAULT_EXPIRES_IN = 24 * 7 * 8;
+
+  /**
    * Room validation schema. See validate.js.
    * @type {Object}
    */
   var roomSchema = {
     roomToken:    String,
     roomUrl:      String,
     roomName:     String,
     maxSize:      Number,
@@ -56,23 +70,23 @@ loop.store = loop.store || {};
    */
   loop.store.RoomStore = loop.store.createStore({
     /**
      * Maximum size given to createRoom; only 2 is supported (and is
      * always passed) because that's what the user-experience is currently
      * designed and tested to handle.
      * @type {Number}
      */
-    maxRoomCreationSize: 2,
+    maxRoomCreationSize: MAX_ROOM_CREATION_SIZE,
 
     /**
      * The number of hours for which the room will exist - default 8 weeks
      * @type {Number}
      */
-    defaultExpiresIn: 24 * 7 * 8,
+    defaultExpiresIn: DEFAULT_EXPIRES_IN,
 
     /**
      * Registered actions.
      * @type {Array}
      */
     actions: [
       "createRoom",
       "createRoomError",
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -51,17 +51,20 @@ describe("loop.conversationViews", funct
         return {
           version: "42",
           channel: "test",
           platform: "test"
         };
       },
       getAudioBlob: sinon.spy(function(name, callback) {
         callback(null, new Blob([new ArrayBuffer(10)], {type: "audio/ogg"}));
-      })
+      }),
+      userProfile: {
+        email: "bob@invalid.tld"
+      }
     };
 
     fakeWindow = {
       navigator: { mozLoop: fakeMozLoop },
       close: sandbox.stub(),
     };
     loop.shared.mixins.setRootObject(fakeWindow);
 
@@ -236,22 +239,25 @@ describe("loop.conversationViews", funct
         sinon.assert.calledWithMatch(dispatcher.dispatch,
           sinon.match.hasOwn("name", "cancelCall"));
       });
   });
 
   describe("CallFailedView", function() {
     var store, fakeAudio;
 
-    function mountTestComponent(props) {
+    var contact = {email: [{value: "test@test.tld"}]};
+
+    function mountTestComponent(options) {
+      options = options || {};
       return TestUtils.renderIntoDocument(
         loop.conversationViews.CallFailedView({
           dispatcher: dispatcher,
           store: store,
-          contact: {email: [{value: "test@test.tld"}]}
+          contact: options.contact
         }));
     }
 
     beforeEach(function() {
       store = new loop.store.ConversationStore(dispatcher, {
         client: {},
         mozLoop: navigator.mozLoop,
         sdkDriver: {}
@@ -261,101 +267,121 @@ describe("loop.conversationViews", funct
         pause: sinon.spy(),
         removeAttribute: sinon.spy()
       };
       sandbox.stub(window, "Audio").returns(fakeAudio);
     });
 
     it("should dispatch a retryCall action when the retry button is pressed",
       function() {
-        view = mountTestComponent();
+        view = mountTestComponent({contact: contact});
 
         var retryBtn = view.getDOMNode().querySelector('.btn-retry');
 
         React.addons.TestUtils.Simulate.click(retryBtn);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithMatch(dispatcher.dispatch,
           sinon.match.hasOwn("name", "retryCall"));
       });
 
     it("should dispatch a cancelCall action when the cancel button is pressed",
       function() {
-        view = mountTestComponent();
+        view = mountTestComponent({contact: contact});
 
         var cancelBtn = view.getDOMNode().querySelector('.btn-cancel');
 
         React.addons.TestUtils.Simulate.click(cancelBtn);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithMatch(dispatcher.dispatch,
           sinon.match.hasOwn("name", "cancelCall"));
       });
 
-    it("should dispatch a fetchEmailLink action when the cancel button is pressed",
+    it("should dispatch a fetchRoomEmailLink action when the email button is pressed",
       function() {
-        view = mountTestComponent();
+        view = mountTestComponent({contact: contact});
 
         var emailLinkBtn = view.getDOMNode().querySelector('.btn-email');
 
         React.addons.TestUtils.Simulate.click(emailLinkBtn);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithMatch(dispatcher.dispatch,
-          sinon.match.hasOwn("name", "fetchEmailLink"));
+          sinon.match.hasOwn("name", "fetchRoomEmailLink"));
+        sinon.assert.calledWithMatch(dispatcher.dispatch,
+          sinon.match.hasOwn("roomOwner", fakeMozLoop.userProfile.email));
+        sinon.assert.calledWithMatch(dispatcher.dispatch,
+          sinon.match.hasOwn("roomName", "test@test.tld"));
+      });
+
+    it("should name the created room using the contact name when available",
+      function() {
+        view = mountTestComponent({contact: {
+          email: [{value: "test@test.tld"}],
+          name: ["Mr Fake ContactName"]
+        }});
+
+        var emailLinkBtn = view.getDOMNode().querySelector('.btn-email');
+
+        React.addons.TestUtils.Simulate.click(emailLinkBtn);
+
+        sinon.assert.calledOnce(dispatcher.dispatch);
+        sinon.assert.calledWithMatch(dispatcher.dispatch,
+          sinon.match.hasOwn("roomName", "Mr Fake ContactName"));
       });
 
     it("should disable the email link button once the action is dispatched",
       function() {
-        view = mountTestComponent();
+        view = mountTestComponent({contact: contact});
         var emailLinkBtn = view.getDOMNode().querySelector('.btn-email');
         React.addons.TestUtils.Simulate.click(emailLinkBtn);
 
         expect(view.getDOMNode().querySelector(".btn-email").disabled).eql(true);
       });
 
     it("should compose an email once the email link is received", function() {
       var composeCallUrlEmail = sandbox.stub(sharedUtils, "composeCallUrlEmail");
-      view = mountTestComponent();
+      view = mountTestComponent({contact: contact});
       store.setStoreState({emailLink: "http://fake.invalid/"});
 
       sinon.assert.calledOnce(composeCallUrlEmail);
       sinon.assert.calledWithExactly(composeCallUrlEmail,
         "http://fake.invalid/", "test@test.tld");
     });
 
     it("should close the conversation window once the email link is received",
       function() {
-        view = mountTestComponent();
+        view = mountTestComponent({contact: contact});
 
         store.setStoreState({emailLink: "http://fake.invalid/"});
 
         sinon.assert.calledOnce(fakeWindow.close);
       });
 
     it("should display an error message in case email link retrieval failed",
       function() {
-        view = mountTestComponent();
+        view = mountTestComponent({contact: contact});
 
         store.trigger("error:emailLink");
 
         expect(view.getDOMNode().querySelector(".error")).not.eql(null);
       });
 
     it("should allow retrying to get a call url if it failed previously",
       function() {
-        view = mountTestComponent();
+        view = mountTestComponent({contact: contact});
 
         store.trigger("error:emailLink");
 
         expect(view.getDOMNode().querySelector(".btn-email").disabled).eql(false);
       });
 
     it("should play a failure sound, once", function() {
-      view = mountTestComponent();
+      view = mountTestComponent({contact: contact});
 
       sinon.assert.calledOnce(navigator.mozLoop.getAudioBlob);
       sinon.assert.calledWithExactly(navigator.mozLoop.getAudioBlob,
                                      "failure", sinon.match.func);
       sinon.assert.calledOnce(fakeAudio.play);
       expect(fakeAudio.loop).to.equal(false);
     });
   });
--- a/browser/components/loop/test/shared/conversationStore_test.js
+++ b/browser/components/loop/test/shared/conversationStore_test.js
@@ -37,16 +37,19 @@ describe("loop.store.ConversationStore",
     };
 
     fakeMozLoop = {
       getLoopPref: sandbox.stub(),
       addConversationContext: sandbox.stub(),
       calls: {
         setCallInProgress: sandbox.stub(),
         clearCallInProgress: sandbox.stub()
+      },
+      rooms: {
+        create: sandbox.stub()
       }
     };
 
     dispatcher = new loop.Dispatcher();
     client = {
       setupOutgoingCall: sinon.stub(),
       requestCallUrl: sinon.stub()
     };
@@ -696,41 +699,53 @@ describe("loop.store.ConversationStore",
         type: "video",
         enabled: false
       }));
 
       expect(store.getStoreState("videoMuted")).eql(true);
     });
   });
 
-  describe("#fetchEmailLink", function() {
+  describe("#fetchRoomEmailLink", function() {
     it("should request a new call url to the server", function() {
-      store.fetchEmailLink(new sharedActions.FetchEmailLink());
+      store.fetchRoomEmailLink(new sharedActions.FetchRoomEmailLink({
+        roomOwner: "bob@invalid.tld",
+        roomName: "FakeRoomName"
+      }));
 
-      sinon.assert.calledOnce(client.requestCallUrl);
-      sinon.assert.calledWith(client.requestCallUrl, "");
+      sinon.assert.calledOnce(fakeMozLoop.rooms.create);
+      sinon.assert.calledWithMatch(fakeMozLoop.rooms.create, {
+        roomOwner: "bob@invalid.tld",
+        roomName: "FakeRoomName"
+      });
     });
 
-    it("should update the emailLink attribute when the new call url is received",
+    it("should update the emailLink attribute when the new room url is received",
       function() {
-        client.requestCallUrl = function(callId, cb) {
-          cb(null, {callUrl: "http://fake.invalid/"});
+        fakeMozLoop.rooms.create = function(roomData, cb) {
+          cb(null, {roomUrl: "http://fake.invalid/"});
         };
-        store.fetchEmailLink(new sharedActions.FetchEmailLink());
+        store.fetchRoomEmailLink(new sharedActions.FetchRoomEmailLink({
+          roomOwner: "bob@invalid.tld",
+          roomName: "FakeRoomName"
+        }));
 
         expect(store.getStoreState("emailLink")).eql("http://fake.invalid/");
       });
 
     it("should trigger an error:emailLink event in case of failure",
       function() {
         var trigger = sandbox.stub(store, "trigger");
-        client.requestCallUrl = function(callId, cb) {
-          cb("error");
+        fakeMozLoop.rooms.create = function(roomData, cb) {
+          cb(new Error("error"));
         };
-        store.fetchEmailLink(new sharedActions.FetchEmailLink());
+        store.fetchRoomEmailLink(new sharedActions.FetchRoomEmailLink({
+          roomOwner: "bob@invalid.tld",
+          roomName: "FakeRoomName"
+        }));
 
         sinon.assert.calledOnce(trigger);
         sinon.assert.calledWithExactly(trigger, "error:emailLink");
       });
   });
 
   describe("Events", function() {
     describe("Websocket progress", function() {
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/xpcshell/test_loopservice_hawk_request.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Unit tests for handling hawkRequest
+ */
+
+"use strict";
+
+Cu.import("resource://services-common/utils.js");
+
+add_task(function* request_with_unicode() {
+  const unicodeName = "yøü";
+
+  loopServer.registerPathHandler("/fake", (request, response) => {
+    let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
+    let jsonBody = JSON.parse(body);
+    Assert.equal(jsonBody.name, CommonUtils.encodeUTF8(unicodeName));
+
+    response.setStatusLine(null, 200, "OK");
+    response.processAsync();
+    response.finish();
+  });
+
+  yield MozLoopServiceInternal.hawkRequestInternal(LOOP_SESSION_TYPE.GUEST, "/fake", "POST", {name: unicodeName}).then(
+    () => Assert.ok(true, "Should have accepted"),
+    () => Assert.ok(false, "Should have accepted"));
+});
+
+function run_test() {
+  setupFakeLoopServer();
+
+  do_register_cleanup(() => {
+    Services.prefs.clearUserPref("loop.hawk-session-token");
+    Services.prefs.clearUserPref("loop.hawk-session-token.fxa");
+    MozLoopService.errors.clear();
+  });
+
+  run_next_test();
+}
--- a/browser/components/loop/test/xpcshell/xpcshell.ini
+++ b/browser/components/loop/test/xpcshell/xpcshell.ini
@@ -6,16 +6,17 @@ skip-if = toolkit == 'gonk'
 
 [test_loopapi_hawk_request.js]
 [test_looppush_initialize.js]
 [test_looprooms.js]
 [test_loopservice_directcall.js]
 [test_loopservice_dnd.js]
 [test_loopservice_expiry.js]
 [test_loopservice_hawk_errors.js]
+[test_loopservice_hawk_request.js]
 [test_loopservice_loop_prefs.js]
 [test_loopservice_initialize.js]
 [test_loopservice_locales.js]
 [test_loopservice_notification.js]
 [test_loopservice_registration.js]
 [test_loopservice_registration_retry.js]
 [test_loopservice_restart.js]
 [test_loopservice_token_invalid.js]
--- a/browser/components/preferences/permissions.xul
+++ b/browser/components/preferences/permissions.xul
@@ -68,15 +68,13 @@
               accesskey="&removepermission.accesskey;"
               icon="remove" label="&removepermission.label;"
               oncommand="gPermissionManager.onPermissionDeleted();"/>
       <button id="removeAllPermissions"
               icon="clear" label="&removeallpermissions.label;"
               accesskey="&removeallpermissions.accesskey;" 
               oncommand="gPermissionManager.onAllPermissionsDeleted();"/>
       <spacer flex="1"/>
-#ifndef XP_MACOSX
       <button oncommand="close();" icon="close"
               label="&button.close.label;" accesskey="&button.close.accesskey;"/>
-#endif
     </hbox>
   </hbox>
 </window>
--- a/browser/components/preferences/translation.xul
+++ b/browser/components/preferences/translation.xul
@@ -75,15 +75,13 @@
               accesskey="&removeSite.accesskey;"
               icon="remove" label="&removeSite.label;"
               oncommand="gTranslationExceptions.onSiteDeleted();"/>
       <button id="removeAllSites"
               icon="clear" label="&removeAllSites.label;"
               accesskey="&removeAllSites.accesskey;"
               oncommand="gTranslationExceptions.onAllSitesDeleted();"/>
       <spacer flex="1"/>
-#ifndef XP_MACOSX
       <button oncommand="close();" icon="close"
               label="&button.close.label;" accesskey="&button.close.accesskey;"/>
-#endif
     </hbox>
   </hbox>
 </window>
--- a/browser/devtools/debugger/test/browser_dbg_chrome-create.js
+++ b/browser/devtools/debugger/test/browser_dbg_chrome-create.js
@@ -5,16 +5,17 @@
  * Tests that a chrome debugger can be created in a new process.
  */
 
 let gProcess;
 
 function test() {
   // Windows XP and 8.1 test slaves are terribly slow at this test.
   requestLongerTimeout(5);
+  Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
 
   initChromeDebugger(aOnClose).then(aProcess => {
     gProcess = aProcess;
 
     info("Starting test...");
     performTest();
   });
 }
@@ -51,10 +52,11 @@ function aOnClose() {
   info("process exit value: " + gProcess._dbgProcess.exitValue);
 
   info("profile path: " + gProcess._dbgProfilePath);
 
   finish();
 }
 
 registerCleanupFunction(function() {
+  Services.prefs.clearUserPref("devtools.debugger.remote-enabled");
   gProcess = null;
 });
--- a/browser/devtools/framework/ToolboxProcess.jsm
+++ b/browser/devtools/framework/ToolboxProcess.jsm
@@ -137,17 +137,19 @@ BrowserToolboxProcess.prototype = {
     if (!this.debuggerServer.initialized) {
       this.debuggerServer.init();
       this.debuggerServer.addBrowserActors();
       dumpn("initialized and added the browser actors for the DebuggerServer.");
     }
 
     let chromeDebuggingPort =
       Services.prefs.getIntPref("devtools.debugger.chrome-debugging-port");
-    this.debuggerServer.openListener(chromeDebuggingPort);
+    let listener = this.debuggerServer.createListener();
+    listener.portOrPath = chromeDebuggingPort;
+    listener.open();
 
     dumpn("Finished initializing the chrome toolbox server.");
     dumpn("Started listening on port: " + chromeDebuggingPort);
   },
 
   /**
    * Initializes a profile for the remote debugger process.
    */
--- a/browser/devtools/framework/connect/connect.js
+++ b/browser/devtools/framework/connect/connect.js
@@ -36,51 +36,55 @@ window.addEventListener("DOMContentLoade
   }
 
   if (port) {
     document.getElementById("port").value = port;
   }
 
   let form = document.querySelector("#connection-form form");
   form.addEventListener("submit", function() {
-    window.submit();
+    window.submit().catch(e => {
+      Cu.reportError(e);
+      // Bug 921850: catch rare exception from DebuggerClient.socketConnect
+      showError("unexpected");
+    });
   });
 }, true);
 
 /**
  * Called when the "connect" button is clicked.
  */
-function submit() {
+let submit = Task.async(function*() {
   // Show the "connecting" screen
   document.body.classList.add("connecting");
 
   let host = document.getElementById("host").value;
   let port = document.getElementById("port").value;
 
   // Save the host/port values
   try {
     Services.prefs.setCharPref("devtools.debugger.remote-host", host);
     Services.prefs.setIntPref("devtools.debugger.remote-port", port);
   } catch(e) {
     // Fails in e10s mode, but not a critical feature.
   }
 
   // Initiate the connection
-  let transport;
-  try {
-    transport = DebuggerClient.socketConnect(host, port);
-  } catch(e) {
-    // Bug 921850: catch rare exception from DebuggerClient.socketConnect
-    showError("unexpected");
-    return;
-  }
+  let transport = yield DebuggerClient.socketConnect({ host, port });
   gClient = new DebuggerClient(transport);
   let delay = Services.prefs.getIntPref("devtools.debugger.remote-timeout");
   gConnectionTimeout = setTimeout(handleConnectionTimeout, delay);
-  gClient.connect(onConnectionReady);
+  let response = yield clientConnect();
+  yield onConnectionReady(...response);
+});
+
+function clientConnect() {
+  let deferred = promise.defer();
+  gClient.connect((...args) => deferred.resolve(args));
+  return deferred.promise;
 }
 
 /**
  * Connection is ready. List actors and build buttons.
  */
 let onConnectionReady = Task.async(function*(aType, aTraits) {
   clearTimeout(gConnectionTimeout);
 
--- a/browser/devtools/framework/toolbox-process-window.js
+++ b/browser/devtools/framework/toolbox-process-window.js
@@ -7,61 +7,62 @@ const { classes: Cc, interfaces: Ci, uti
 
 let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
 let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
 let { DebuggerClient } =
   Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {});
 let { ViewHelpers } =
   Cu.import("resource:///modules/devtools/ViewHelpers.jsm", {});
+let { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
 
 /**
  * Shortcuts for accessing various debugger preferences.
  */
 let Prefs = new ViewHelpers.Prefs("devtools.debugger", {
   chromeDebuggingHost: ["Char", "chrome-debugging-host"],
   chromeDebuggingPort: ["Int", "chrome-debugging-port"]
 });
 
 let gToolbox, gClient;
 
-function connect() {
+let connect = Task.async(function*() {
   window.removeEventListener("load", connect);
   // Initiate the connection
-  let transport = DebuggerClient.socketConnect(
-    Prefs.chromeDebuggingHost,
-    Prefs.chromeDebuggingPort
-  );
+  let transport = yield DebuggerClient.socketConnect({
+    host: Prefs.chromeDebuggingHost,
+    port: Prefs.chromeDebuggingPort
+  });
   gClient = new DebuggerClient(transport);
   gClient.connect(() => {
     let addonID = getParameterByName("addonID");
 
     if (addonID) {
       gClient.listAddons(({addons}) => {
         let addonActor = addons.filter(addon => addon.id === addonID).pop();
         openToolbox(addonActor);
       });
     } else {
       gClient.listTabs(openToolbox);
     }
   });
-}
+});
 
 // Certain options should be toggled since we can assume chrome debugging here
 function setPrefDefaults() {
   Services.prefs.setBoolPref("devtools.inspector.showUserAgentStyles", true);
   Services.prefs.setBoolPref("devtools.profiler.ui.show-platform-data", true);
   Services.prefs.setBoolPref("browser.devedition.theme.showCustomizeButton", false);
 }
 
 window.addEventListener("load", function() {
   let cmdClose = document.getElementById("toolbox-cmd-close");
   cmdClose.addEventListener("command", onCloseCommand);
   setPrefDefaults();
-  connect();
+  connect().catch(Cu.reportError);
 });
 
 function onCloseCommand(event) {
   window.close();
 }
 
 function openToolbox(form) {
   let options = {
--- a/browser/devtools/performance/test/browser_perf-details.js
+++ b/browser/devtools/performance/test/browser_perf-details.js
@@ -20,27 +20,25 @@ function spawnTest () {
 
   // Select waterfall view
   viewChanged = onceSpread(DetailsView, EVENTS.DETAILS_VIEW_SELECTED);
   command($("toolbarbutton[data-view='waterfall']"));
   [_, viewName] = yield viewChanged;
   is(viewName, "waterfall", "DETAILS_VIEW_SELECTED fired with view name");
   checkViews(DetailsView, doc, "waterfall");
 
-
   yield teardown(panel);
   finish();
 }
 
 function checkViews (DetailsView, doc, currentView) {
-  for (let viewName in DetailsView.views) {
-    let view = DetailsView.views[viewName].el;
-    let button = doc.querySelector("toolbarbutton[data-view='" + viewName + "']");
+  for (let viewName in DetailsView.viewIndexes) {
+    let button = doc.querySelector(`toolbarbutton[data-view="${viewName}"]`);
 
+    is(DetailsView.el.selectedIndex, DetailsView.viewIndexes[currentView],
+      `DetailsView correctly has ${currentView} selected.`);
     if (viewName === currentView) {
-      ok(!view.getAttribute("hidden"), view + " view displayed");
-      ok(button.getAttribute("checked"), view + " button checked");
+      ok(button.getAttribute("checked"), `${viewName} button checked`);
     } else {
-      ok(view.getAttribute("hidden"), view + " view hidden");
-      ok(!button.getAttribute("checked"), view + " button not checked");
+      ok(!button.getAttribute("checked"), `${viewName} button not checked`);
     }
   }
 }
--- a/browser/devtools/performance/views/details.js
+++ b/browser/devtools/performance/views/details.js
@@ -40,16 +40,24 @@ let DetailsView = {
    * Select one of the DetailView's subviews to be rendered,
    * hiding the others.
    *
    * @params {String} selectedView
    *         Name of the view to be shown.
    */
   selectView: function (selectedView) {
     this.el.selectedIndex = this.viewIndexes[selectedView];
+
+    for (let button of $$("toolbarbutton[data-view]", $("#details-toolbar"))) {
+      if (button.getAttribute("data-view") === selectedView)
+        button.setAttribute("checked", true);
+      else
+        button.removeAttribute("checked");
+    }
+
     this.emit(EVENTS.DETAILS_VIEW_SELECTED, selectedView);
   },
 
   /**
    * Called when a view button is clicked.
    */
   _onViewToggle: function (e) {
     this.selectView(e.target.getAttribute("data-view"));
--- a/browser/devtools/scratchpad/scratchpad.js
+++ b/browser/devtools/scratchpad/scratchpad.js
@@ -511,17 +511,17 @@ var Scratchpad = {
   {
     let deferred = promise.defer();
     let reject = aReason => deferred.reject(aReason);
 
     this.execute().then(([aString, aError, aResult]) => {
       let resolve = () => deferred.resolve([aString, aError, aResult]);
 
       if (aError) {
-        this.writeAsErrorComment(aError.exception).then(resolve, reject);
+        this.writeAsErrorComment(aError).then(resolve, reject);
       }
       else {
         this.editor.dropSelection();
         resolve();
       }
     }, reject);
 
     return deferred.promise;
@@ -538,17 +538,17 @@ var Scratchpad = {
   {
     let deferred = promise.defer();
     let reject = aReason => deferred.reject(aReason);
 
     this.execute().then(([aString, aError, aResult]) => {
       let resolve = () => deferred.resolve([aString, aError, aResult]);
 
       if (aError) {
-        this.writeAsErrorComment(aError.exception).then(resolve, reject);
+        this.writeAsErrorComment(aError).then(resolve, reject);
       }
       else {
         this.editor.dropSelection();
         this.sidebar.open(aString, aResult).then(resolve, reject);
       }
     }, reject);
 
     return deferred.promise;
@@ -603,17 +603,17 @@ var Scratchpad = {
   {
     let deferred = promise.defer();
     let reject = aReason => deferred.reject(aReason);
 
     this.execute().then(([aString, aError, aResult]) => {
       let resolve = () => deferred.resolve([aString, aError, aResult]);
 
       if (aError) {
-        this.writeAsErrorComment(aError.exception).then(resolve, reject);
+        this.writeAsErrorComment(aError).then(resolve, reject);
       }
       else if (VariablesView.isPrimitive({ value: aResult })) {
         this._writePrimitiveAsComment(aResult).then(resolve, reject);
       }
       else {
         let objectClient = new ObjectClient(this.debuggerClient, aResult);
         objectClient.getDisplayString(aResponse => {
           if (aResponse.error) {
@@ -664,17 +664,17 @@ var Scratchpad = {
     const onReply = ({ data }) => {
       if (data.id !== id) {
         return;
       }
       this.prettyPrintWorker.removeEventListener("message", onReply, false);
 
       if (data.error) {
         let errorString = DevToolsUtils.safeErrorString(data.error);
-        this.writeAsErrorComment(errorString);
+        this.writeAsErrorComment({ exception: errorString });
         deferred.reject(errorString);
       } else {
         this.editor.setText(data.code);
         deferred.resolve(data.code);
       }
     };
 
     this.prettyPrintWorker.addEventListener("message", onReply, false);
@@ -691,17 +691,17 @@ var Scratchpad = {
   /**
    * Parse the text and return an AST. If we can't parse it, write an error
    * comment and return false.
    */
   _parseText: function SP__parseText(aText) {
     try {
       return Reflect.parse(aText);
     } catch (e) {
-      this.writeAsErrorComment(DevToolsUtils.safeErrorString(e));
+      this.writeAsErrorComment({ exception: DevToolsUtils.safeErrorString(e) });
       return false;
     }
   },
 
   /**
    * Determine if the given AST node location contains the given cursor
    * position.
    *
@@ -908,43 +908,54 @@ var Scratchpad = {
 
     let [ from, to ] = this.editor.getPosition(text.length, (text + value).length);
     this.editor.setSelection(from, to);
   },
 
   /**
    * Write out an error at the current insertion point as a block comment
    * @param object aValue
-   *        The Error object to write out the message and stack trace
+   *        The error object to write out the message and stack trace. It must
+   *        contain an |exception| property with the actual error thrown, but it
+   *        will often be the entire response of an evaluateJS request.
    * @return Promise
    *         The promise that indicates when writing the comment completes.
    */
   writeAsErrorComment: function SP_writeAsErrorComment(aError)
   {
     let deferred = promise.defer();
 
-    if (VariablesView.isPrimitive({ value: aError })) {
-      let type = aError.type;
+    if (VariablesView.isPrimitive({ value: aError.exception })) {
+      let error = aError.exception;
+      let type = error.type;
       if (type == "undefined" ||
           type == "null" ||
           type == "Infinity" ||
           type == "-Infinity" ||
           type == "NaN" ||
           type == "-0") {
         deferred.resolve(type);
       }
       else if (type == "longString") {
-        deferred.resolve(aError.initial + "\u2026");
+        deferred.resolve(error.initial + "\u2026");
       }
       else {
-        deferred.resolve(aError);
+        deferred.resolve(error);
       }
-    }
-    else {
-      let objectClient = new ObjectClient(this.debuggerClient, aError);
+    } else if ("preview" in aError.exception) {
+      let error = aError.exception;
+      let stack = this._constructErrorStack(error.preview);
+      if (typeof aError.exceptionMessage == "string") {
+        deferred.resolve(aError.exceptionMessage + stack);
+      } else {
+        deferred.resolve(stack);
+      }
+    } else {
+      // If there is no preview information, we need to ask the server for more.
+      let objectClient = new ObjectClient(this.debuggerClient, aError.exception);
       objectClient.getPrototypeAndProperties(aResponse => {
         if (aResponse.error) {
           deferred.reject(aResponse);
           return;
         }
 
         let { ownProperties, safeGetterValues } = aResponse;
         let error = Object.create(null);
@@ -953,32 +964,17 @@ var Scratchpad = {
         for (let key of Object.keys(safeGetterValues)) {
           error[key] = safeGetterValues[key].getterValue;
         }
 
         for (let key of Object.keys(ownProperties)) {
           error[key] = ownProperties[key].value;
         }
 
-        // Assemble the best possible stack we can given the properties we have.
-        let stack;
-        if (typeof error.stack == "string" && error.stack) {
-          stack = error.stack;
-        }
-        else if (typeof error.fileName == "string") {
-          stack = "@" + error.fileName;
-          if (typeof error.lineNumber == "number") {
-            stack += ":" + error.lineNumber;
-          }
-        }
-        else if (typeof error.lineNumber == "number") {
-          stack = "@" + error.lineNumber;
-        }
-
-        stack = stack ? "\n" + stack.replace(/\n$/, "") : "";
+        let stack = this._constructErrorStack(error);
 
         if (typeof error.message == "string") {
           deferred.resolve(error.message + stack);
         }
         else {
           objectClient.getDisplayString(aResponse => {
             if (aResponse.error) {
               deferred.reject(aResponse);
@@ -995,16 +991,47 @@ var Scratchpad = {
     }
 
     return deferred.promise.then(aMessage => {
       console.error(aMessage);
       this.writeAsComment("Exception: " + aMessage);
     });
   },
 
+  /**
+   * Assembles the best possible stack from the properties of the provided
+   * error.
+   */
+  _constructErrorStack(error) {
+    let stack;
+    if (typeof error.stack == "string" && error.stack) {
+      stack = error.stack;
+    } else if (typeof error.fileName == "string") {
+      stack = "@" + error.fileName;
+      if (typeof error.lineNumber == "number") {
+        stack += ":" + error.lineNumber;
+      }
+    } else if (typeof error.filename == "string") {
+      stack = "@" + error.filename;
+      if (typeof error.lineNumber == "number") {
+        stack += ":" + error.lineNumber;
+        if (typeof error.columnNumber == "number") {
+          stack += ":" + error.columnNumber;
+        }
+      }
+    } else if (typeof error.lineNumber == "number") {
+      stack = "@" + error.lineNumber;
+      if (typeof error.columnNumber == "number") {
+        stack += ":" + error.columnNumber;
+      }
+    }
+
+    return stack ? "\n" + stack.replace(/\n$/, "") : "";
+  },
+
   // Menu Operations
 
   /**
    * Open a new Scratchpad window.
    *
    * @return nsIWindow
    */
   openScratchpad: function SP_openScratchpad()
--- a/browser/devtools/scratchpad/test/browser_scratchpad_display_non_error_exceptions.js
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_display_non_error_exceptions.js
@@ -36,17 +36,17 @@ function runTests()
     result: message + openComment + "Hello World!" + closeComment,
     label: "message display output"
   },
   {
     // Display error1, throw new Error("Ouch")
     method: "display",
     code: error1,
     result: error1 + openComment +
-            "Exception: Ouch!\n@" + scratchpad.uniqueName + ":1:7" + closeComment,
+            "Exception: Error: Ouch!\n@" + scratchpad.uniqueName + ":1:7" + closeComment,
     label: "error display output"
   },
   {
     // Display error2, throw "A thrown string"
     method: "display",
     code: error2,
     result: error2 + openComment + "Exception: A thrown string" + closeComment,
     label: "thrown string display output"
@@ -57,34 +57,34 @@ function runTests()
     code: error3,
     result: error3 + openComment + "Exception: [object Object]" + closeComment,
     label: "thrown object display output"
   },
   {
     // Display error4, document.body.appendChild(document.body)
     method: "display",
     code: error4,
-    result: error4 + openComment + "Exception: Node cannot be inserted " +
+    result: error4 + openComment + "Exception: HierarchyRequestError: Node cannot be inserted " +
             "at the specified point in the hierarchy\n@" +
             scratchpad.uniqueName + ":1:0" + closeComment,
     label: "Alternative format error display output"
   },
   {
     // Run message
     method: "run",
     code: message,
     result: message,
     label: "message run output"
   },
   {
     // Run error1, throw new Error("Ouch")
     method: "run",
     code: error1,
     result: error1 + openComment +
-            "Exception: Ouch!\n@" + scratchpad.uniqueName + ":1:7" + closeComment,
+            "Exception: Error: Ouch!\n@" + scratchpad.uniqueName + ":1:7" + closeComment,
     label: "error run output"
   },
   {
     // Run error2, throw "A thrown string"
     method: "run",
     code: error2,
     result: error2 + openComment + "Exception: A thrown string" + closeComment,
     label: "thrown string run output"
@@ -95,16 +95,16 @@ function runTests()
     code: error3,
     result: error3 + openComment + "Exception: [object Object]" + closeComment,
     label: "thrown object run output"
   },
   {
     // Run error4, document.body.appendChild(document.body)
     method: "run",
     code: error4,
-    result: error4 + openComment + "Exception: Node cannot be inserted " +
+    result: error4 + openComment + "Exception: HierarchyRequestError: Node cannot be inserted " +
             "at the specified point in the hierarchy\n@" +
             scratchpad.uniqueName + ":1:0" + closeComment,
     label: "Alternative format error run output"
   }];
 
   runAsyncTests(scratchpad, tests).then(finish);
 }
--- a/browser/devtools/scratchpad/test/browser_scratchpad_display_outputs_errors.js
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_display_outputs_errors.js
@@ -31,42 +31,42 @@ function runTests()
     method: "display",
     code: message,
     result: message + openComment + "Hello World!" + closeComment,
     label: "message display output"
   },
   {
     method: "display",
     code: error,
-    result: error + openComment + "Exception: Ouch!\n@" +
+    result: error + openComment + "Exception: Error: Ouch!\n@" +
             scratchpad.uniqueName + ":1:7" + closeComment,
     label: "error display output",
   },
   {
     method: "display",
     code: syntaxError,
-    result: syntaxError + openComment + "Exception: expected expression, got end of script\n@" +
+    result: syntaxError + openComment + "Exception: SyntaxError: expected expression, got end of script\n@" +
             scratchpad.uniqueName + ":1" + closeComment,
     label: "syntaxError display output",
   },
   {
     method: "run",
     code: message,
     result: message,
     label: "message run output",
   },
   {
     method: "run",
     code: error,
-    result: error + openComment + "Exception: Ouch!\n@" +
+    result: error + openComment + "Exception: Error: Ouch!\n@" +
             scratchpad.uniqueName + ":1:7" + closeComment,
     label: "error run output",
   },
   {
     method: "run",
     code: syntaxError,
-    result: syntaxError + openComment + "Exception: expected expression, got end of script\n@" +
+    result: syntaxError + openComment + "Exception: SyntaxError: expected expression, got end of script\n@" +
             scratchpad.uniqueName + ":1" + closeComment,
     label: "syntaxError run output",
   }];
 
   runAsyncTests(scratchpad, tests).then(finish);
 }
--- a/browser/devtools/scratchpad/test/head.js
+++ b/browser/devtools/scratchpad/test/head.js
@@ -4,16 +4,17 @@
 
 "use strict";
 
 const {NetUtil} = Cu.import("resource://gre/modules/NetUtil.jsm", {});
 const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
 const {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
 const {require} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
+const {DevToolsUtils} = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {});
 
 let gScratchpadWindow; // Reference to the Scratchpad chrome window object
 
 gDevTools.testing = true;
 SimpleTest.registerCleanupFunction(() => {
   gDevTools.testing = false;
 });
 
--- a/browser/devtools/shared/widgets/Tooltip.js
+++ b/browser/devtools/shared/widgets/Tooltip.js
@@ -410,16 +410,26 @@ Tooltip.prototype = {
 
   _onBaseNodeMouseMove: function(event) {
     if (event.target !== this._lastHovered) {
       this.hide();
       this._lastHovered = event.target;
       setNamedTimeout(this.uid, this._showDelay, () => {
         this.isValidHoverTarget(event.target).then(target => {
           this.show(target);
+        }).catch((reason) => {
+          if (reason === false) {
+            // isValidHoverTarget rejects with false if the tooltip should
+            // not be shown. This can be safely ignored.
+            return;
+          }
+          // Report everything else. Reason might be error that should not be
+          // hidden.
+          console.error("isValidHoverTarget rejected with an unexpected reason:");
+          console.error(reason);
         });
       });
     }
   },
 
   /**
    * Is the given target DOMNode a valid node for toggling the tooltip on hover.
    * This delegates to the user-defined _targetNodeCb callback.
--- a/browser/devtools/webide/modules/app-manager.js
+++ b/browser/devtools/webide/modules/app-manager.js
@@ -341,16 +341,18 @@ let AppManager = exports.AppManager = {
           deferred.resolve();
         } else {
           deferred.reject();
         }
       }
       this.connection.on(Connection.Events.CONNECTED, onConnectedOrDisconnected);
       this.connection.on(Connection.Events.DISCONNECTED, onConnectedOrDisconnected);
       try {
+        // Reset the connection's state to defaults
+        this.connection.resetOptions();
         this.selectedRuntime.connect(this.connection).then(
           () => {},
           deferred.reject.bind(deferred));
       } catch(e) {
         console.error(e);
         deferred.reject();
       }
     }, deferred.reject);
--- a/browser/devtools/webide/modules/runtimes.js
+++ b/browser/devtools/webide/modules/runtimes.js
@@ -444,16 +444,17 @@ WiFiRuntime.prototype = {
   type: RuntimeTypes.WIFI,
   connect: function(connection) {
     let service = discovery.getRemoteService("devtools", this.deviceName);
     if (!service) {
       return promise.reject("Can't find device: " + this.name);
     }
     connection.host = service.host;
     connection.port = service.port;
+    connection.encryption = service.encryption;
     connection.connect();
     return promise.resolve();
   },
   get id() {
     return this.deviceName;
   },
   get name() {
     return this.deviceName;
--- a/browser/metro/base/content/browser-ui.js
+++ b/browser/metro/base/content/browser-ui.js
@@ -231,17 +231,19 @@ var BrowserUI = {
    */
   runDebugServer: function runDebugServer(aPort) {
     let port = aPort || Services.prefs.getIntPref(debugServerPortChanged);
     if (!DebuggerServer.initialized) {
       DebuggerServer.init();
       DebuggerServer.addBrowserActors();
       DebuggerServer.addActors('chrome://browser/content/dbg-metro-actors.js');
     }
-    DebuggerServer.openListener(port);
+    let listener = DebuggerServer.createListener();
+    listener.portOrPath = port;
+    listener.open();
   },
 
   stopDebugServer: function stopDebugServer() {
     if (DebuggerServer.initialized) {
       DebuggerServer.destroy();
     }
   },
 
--- a/browser/modules/UITour.jsm
+++ b/browser/modules/UITour.jsm
@@ -1640,17 +1640,17 @@ this.UITour = {
   },
 
   notify(eventName, params) {
     let winEnum = Services.wm.getEnumerator("navigator:browser");
     while (winEnum.hasMoreElements()) {
       let window = winEnum.getNext();
       if (window.closed)
         continue;
-debugger;
+
       let originTabs = this.originTabs.get(window);
       if (!originTabs)
         continue;
 
       for (let tab of originTabs) {
         let messageManager = tab.linkedBrowser.messageManager;
         let detail = {
           event: eventName,
--- a/browser/modules/webrtcUI.jsm
+++ b/browser/modules/webrtcUI.jsm
@@ -65,16 +65,23 @@ this.webrtcUI = {
       let browser = aStream.browser;
       let browserWindow = browser.ownerDocument.defaultView;
       let tab = browserWindow.gBrowser &&
                 browserWindow.gBrowser.getTabForBrowser(browser);
       return {uri: state.documentURI, tab: tab, browser: browser, types: types};
     });
   },
 
+  swapBrowserForNotification: function(aOldBrowser, aNewBrowser) {
+    for (let stream of this._streams) {
+      if (stream.browser == aOldBrowser)
+        stream.browser = aNewBrowser;
+    };
+  },
+
   showSharingDoorhanger: function(aActiveStream, aType) {
     let browserWindow = aActiveStream.browser.ownerDocument.defaultView;
     if (aActiveStream.tab) {
       browserWindow.gBrowser.selectedTab = aActiveStream.tab;
     } else {
       aActiveStream.browser.focus();
     }
     browserWindow.focus();
@@ -179,41 +186,43 @@ function prompt(aBrowser, aRequest) {
   let mainLabel;
   if (sharingScreen) {
     mainLabel = stringBundle.getString("getUserMedia.shareSelectedItems.label");
   }
   else {
     let string = stringBundle.getString("getUserMedia.shareSelectedDevices.label");
     mainLabel = PluralForm.get(requestTypes.length, string);
   }
+
+  let notification; // Used by action callbacks.
   let mainAction = {
     label: mainLabel,
     accessKey: stringBundle.getString("getUserMedia.shareSelectedDevices.accesskey"),
     // The real callback will be set during the "showing" event. The
     // empty function here is so that PopupNotifications.show doesn't
     // reject the action.
     callback: function() {}
   };
 
   let secondaryActions = [
     {
       label: stringBundle.getString("getUserMedia.denyRequest.label"),
       accessKey: stringBundle.getString("getUserMedia.denyRequest.accesskey"),
       callback: function () {
-        denyRequest(aBrowser, aRequest);
+        denyRequest(notification.browser, aRequest);
       }
     }
   ];
 
   if (!sharingScreen) { // Bug 1037438: implement 'never' for screen sharing.
     secondaryActions.push({
       label: stringBundle.getString("getUserMedia.never.label"),
       accessKey: stringBundle.getString("getUserMedia.never.accesskey"),
       callback: function () {
-        denyRequest(aBrowser, aRequest);
+        denyRequest(notification.browser, aRequest);
         // Let someone save "Never" for http sites so that they can be stopped from
         // bothering you with doorhangers.
         let perms = Services.perms;
         if (audioDevices.length)
           perms.add(uri, "microphone", perms.DENY_ACTION);
         if (videoDevices.length)
           perms.add(uri, "camera", perms.DENY_ACTION);
       }
@@ -277,20 +286,20 @@ function prompt(aBrowser, aRequest) {
         // and will grant audio access immediately.
         if ((!audioDevices.length || micPerm) && (!videoDevices.length || camPerm)) {
           // All permissions we were about to request are already persistently set.
           let allowedDevices = [];
           if (videoDevices.length && camPerm == perms.ALLOW_ACTION)
             allowedDevices.push(videoDevices[0].deviceIndex);
           if (audioDevices.length && micPerm == perms.ALLOW_ACTION)
             allowedDevices.push(audioDevices[0].deviceIndex);
-          aBrowser.messageManager.sendAsyncMessage("webrtc:Allow",
-                                                   {callID: aRequest.callID,
-                                                    windowID: aRequest.windowID,
-                                                    devices: allowedDevices});
+          let mm = this.browser.messageManager;
+          mm.sendAsyncMessage("webrtc:Allow", {callID: aRequest.callID,
+                                               windowID: aRequest.windowID,
+                                               devices: allowedDevices});
           this.remove();
           return true;
         }
       }
 
       function listDevices(menupopup, devices) {
         while (menupopup.lastChild)
           menupopup.removeChild(menupopup.lastChild);
@@ -399,36 +408,38 @@ function prompt(aBrowser, aRequest) {
             allowedDevices.push(audioDeviceIndex);
           if (aRemember) {
             perms.add(uri, "microphone",
                       allowMic ? perms.ALLOW_ACTION : perms.DENY_ACTION);
           }
         }
 
         if (!allowedDevices.length) {
-          denyRequest(aBrowser, aRequest);
+          denyRequest(notification.browser, aRequest);
           return;
         }
 
-        aBrowser.messageManager.sendAsyncMessage("webrtc:Allow",
-                                                 {callID: aRequest.callID,
-                                                  windowID: aRequest.windowID,
-                                                  devices: allowedDevices});
+        let mm = notification.browser.messageManager
+        mm.sendAsyncMessage("webrtc:Allow", {callID: aRequest.callID,
+                                             windowID: aRequest.windowID,
+                                             devices: allowedDevices});
       };
       return false;
     }
   };
 
   let anchorId = "webRTC-shareDevices-notification-icon";
   if (requestTypes.length == 1 && requestTypes[0] == "Microphone")
     anchorId = "webRTC-shareMicrophone-notification-icon";
   if (requestTypes.indexOf("Screen") != -1)
     anchorId = "webRTC-shareScreen-notification-icon";
-  chromeWin.PopupNotifications.show(aBrowser, "webRTC-shareDevices", message,
-                                    anchorId, mainAction, secondaryActions, options);
+  notification =
+    chromeWin.PopupNotifications.show(aBrowser, "webRTC-shareDevices", message,
+                                      anchorId, mainAction, secondaryActions,
+                                      options);
 }
 
 function getGlobalIndicator() {
 #ifndef XP_MACOSX
   const INDICATOR_CHROME_URI = "chrome://browser/content/webrtcIndicator.xul";
   const features = "chrome,dialog=yes,titlebar=no,popup=yes";
 
   return Services.ww.openWindow(null, INDICATOR_CHROME_URI, "_blank", features, []);
@@ -696,16 +707,17 @@ function updateBrowserSpecificIndicator(
   } else if (aState.microphone) {
     captureState = "Microphone";
   }
 
   let chromeWin = aBrowser.ownerDocument.defaultView;
   let stringBundle = chromeWin.gNavigatorBundle;
 
   let windowId = aState.windowId;
+  let notification; // Used by action callbacks.
   let mainAction = {
     label: stringBundle.getString("getUserMedia.continueSharing.label"),
     accessKey: stringBundle.getString("getUserMedia.continueSharing.accesskey"),
     callback: function () {},
     dismiss: true
   };
   let secondaryActions = [{
     label: stringBundle.getString("getUserMedia.stopSharing.label"),
@@ -716,73 +728,89 @@ function updateBrowserSpecificIndicator(
       let perms = Services.perms;
       if (aState.camera &&
           perms.testExactPermission(uri, "camera") == perms.ALLOW_ACTION)
         perms.remove(host, "camera");
       if (aState.microphone &&
           perms.testExactPermission(uri, "microphone") == perms.ALLOW_ACTION)
         perms.remove(host, "microphone");
 
-      aBrowser.messageManager.sendAsyncMessage("webrtc:StopSharing", windowId);
+      let mm = notification.browser.messageManager;
+      mm.sendAsyncMessage("webrtc:StopSharing", windowId);
     }
   }];
   let options = {
     hideNotNow: true,
     dismissed: true,
-    eventCallback: function(aTopic) {
+    eventCallback: function(aTopic, aNewBrowser) {
       if (aTopic == "shown") {
         let PopupNotifications = this.browser.ownerDocument.defaultView.PopupNotifications;
         let popupId = captureState == "Microphone" ? "Microphone" : "Devices";
         PopupNotifications.panel.firstChild.setAttribute("popupid", "webRTC-sharing" + popupId);
       }
-      return aTopic == "swapping";
+
+      if (aTopic == "swapping") {
+        webrtcUI.swapBrowserForNotification(this.browser, aNewBrowser);
+        return true;
+      }
+
+      return false;
     }
   };
   if (captureState) {
     let anchorId = captureState == "Microphone" ? "webRTC-sharingMicrophone-notification-icon"
                                                 : "webRTC-sharingDevices-notification-icon";
     let message = stringBundle.getString("getUserMedia.sharing" + captureState + ".message2");
-    chromeWin.PopupNotifications.show(aBrowser, "webRTC-sharingDevices", message,
-                                      anchorId, mainAction, secondaryActions, options);
+    notification =
+      chromeWin.PopupNotifications.show(aBrowser, "webRTC-sharingDevices", message,
+                                        anchorId, mainAction, secondaryActions, options);
   }
   else {
     removeBrowserNotification(aBrowser,"webRTC-sharingDevices");
   }
 
   // Now handle the screen sharing indicator.
   if (!aState.screen) {
     removeBrowserNotification(aBrowser,"webRTC-sharingScreen");
     return;
   }
 
+  let screenSharingNotif; // Used by action callbacks.
   options = {
     hideNotNow: true,
     dismissed: true,
-    eventCallback: function(aTopic) {
+    eventCallback: function(aTopic, aNewBrowser) {
       if (aTopic == "shown") {
         let PopupNotifications = this.browser.ownerDocument.defaultView.PopupNotifications;
         PopupNotifications.panel.firstChild.setAttribute("popupid", "webRTC-sharingScreen");
       }
-      return aTopic == "swapping";
+
+      if (aTopic == "swapping") {
+        webrtcUI.swapBrowserForNotification(this.browser, aNewBrowser);
+        return true;
+      }
+
+      return false;
     }
   };
   secondaryActions = [{
     label: stringBundle.getString("getUserMedia.stopSharing.label"),
     accessKey: stringBundle.getString("getUserMedia.stopSharing.accesskey"),
     callback: function () {
-      aBrowser.messageManager.sendAsyncMessage("webrtc:StopSharing",
-                                               "screen:" + windowId);
+      let mm = screenSharingNotif.browser.messageManager;
+      mm.sendAsyncMessage("webrtc:StopSharing", "screen:" + windowId);
     }
   }];
   // If we are sharing both a window and the screen, we show 'Screen'.
   let stringId = "getUserMedia.sharing" + aState.screen;
-  chromeWin.PopupNotifications.show(aBrowser, "webRTC-sharingScreen",
-                                    stringBundle.getString(stringId + ".message"),
-                                    "webRTC-sharingScreen-notification-icon",
-                                    mainAction, secondaryActions, options);
+  screenSharingNotif =
+    chromeWin.PopupNotifications.show(aBrowser, "webRTC-sharingScreen",
+                                      stringBundle.getString(stringId + ".message"),
+                                      "webRTC-sharingScreen-notification-icon",
+                                      mainAction, secondaryActions, options);
 }
 
 function removeBrowserNotification(aBrowser, aNotificationId) {
   let win = aBrowser.ownerDocument.defaultView;
   let notification =
     win.PopupNotifications.getNotification(aNotificationId, aBrowser);
   if (notification)
     win.PopupNotifications.remove(notification);
--- a/browser/themes/windows/searchbar.css
+++ b/browser/themes/windows/searchbar.css
@@ -140,16 +140,17 @@ searchbar[oneoffui] .search-go-button:-m
 
 .search-panel-input-value {
   color: black;
 }
 
 .search-panel-one-offs {
   margin: 0 0 !important;
   border-top: 1px solid #ccc;
+  line-height: 0;
 }
 
 .searchbar-engine-one-off-item {
   -moz-appearance: none;
   display: inline-block;
   border: none;
   min-width: 48px;
   height: 32px;
--- a/dom/bindings/test/test_exceptions_from_jsimplemented.html
+++ b/dom/bindings/test/test_exceptions_from_jsimplemented.html
@@ -7,30 +7,44 @@ https://bugzilla.mozilla.org/show_bug.cg
   <meta charset="utf-8">
   <title>Test for Bug 923010</title>
   <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
   <script type="application/javascript">
   /** Test for Bug 923010 **/
   try {
     var conn = new mozRTCPeerConnection();
-    var candidate = new mozRTCIceCandidate({candidate: null });
     try {
-      conn.addIceCandidate(candidate, function() {
-          ok(false, "The call to addIceCandidate succeeded when it should have thrown");
+      conn.updateIce(candidate, function() {
+          ok(false, "The call to updateIce succeeded when it should have thrown");
         }, function() {
-          ok(false, "The call to addIceCandidate failed when it should have thrown");
+          ok(false, "The call to updateIce failed when it should have thrown");
         })
-      ok(false, "That call to addIceCandidate should have thrown");
+      ok(false, "That call to updateIce should have thrown");
     } catch (e) {
-      is(e.lineNumber, 17, "Exception should have been on line 17");
+      is(e.lineNumber, 16, "Exception should have been on line 16");
       is(e.message,
-         "Invalid candidate passed to addIceCandidate!",
+         "updateIce not yet implemented",
          "Should have the exception we expect");
     }
+
+    var candidate = new mozRTCIceCandidate({candidate: null });
+
+    conn.addIceCandidate(candidate)
+    .then(function() {
+      ok(false, "addIceCandidate succeeded when it should have failed");
+    }, function(reason) {
+      is(reason.lineNumber, 31, "Rejection should have been on line 31");
+      is(reason.message,
+         "Invalid candidate passed to addIceCandidate!",
+         "Should have the rejection we expect");
+    })
+    .catch(function(reason) {
+      ok(false, "unexpected error: " + reason);
+    });
   } catch (e) {
     // b2g has no WebRTC, apparently
     todo(false, "No WebRTC on b2g yet");
   }
   </script>
 </head>
 <body>
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=923010">Mozilla Bug 923010</a>
--- a/dom/media/PeerConnection.js
+++ b/dom/media/PeerConnection.js
@@ -337,17 +337,17 @@ RTCPeerConnection.prototype = {
     if (!rtcConfig.iceServers ||
         !Services.prefs.getBoolPref("media.peerconnection.use_document_iceservers")) {
       rtcConfig.iceServers =
         JSON.parse(Services.prefs.getCharPref("media.peerconnection.default_iceservers"));
     }
     this._mustValidateRTCConfiguration(rtcConfig,
         "RTCPeerConnection constructor passed invalid RTCConfiguration");
     if (_globalPCList._networkdown || !this._win.navigator.onLine) {
-      throw new this._win.DOMError("",
+      throw new this._win.DOMError("InvalidStateError",
           "Can't create RTCPeerConnections when the network is down");
     }
 
     this.makeGetterSetterEH("onaddstream");
     this.makeGetterSetterEH("onaddtrack");
     this.makeGetterSetterEH("onicecandidate");
     this.makeGetterSetterEH("onnegotiationneeded");
     this.makeGetterSetterEH("onsignalingstatechange");
@@ -380,37 +380,22 @@ RTCPeerConnection.prototype = {
     this._impl.initialize(this._observer, this._win, rtcConfig,
                           Services.tm.currentThread);
     this._initIdp();
     _globalPCList.notifyLifecycleObservers(this, "initialized");
   },
 
   get _impl() {
     if (!this._pc) {
-      throw new this._win.DOMError("",
+      throw new this._win.DOMError("InvalidStateError",
           "RTCPeerConnection is gone (did you enter Offline mode?)");
     }
     return this._pc;
   },
 
-  callCB: function(callback, arg) {
-    if (callback) {
-      this._win.setTimeout(() => {
-        try {
-          callback(arg);
-        } catch(e) {
-          // A content script (user-provided) callback threw an error. We don't
-          // want this to take down peerconnection, but we still want the user
-          // to see it, so we catch it, report it, and move on.
-          this.logErrorAndCallOnError(e.message, e.fileName, e.lineNumber);
-        }
-      }, 0);
-    }
-  },
-
   _initIdp: function() {
     let prefName = "media.peerconnection.identity.timeout";
     let idpTimeout = Services.prefs.getIntPref(prefName);
     let warningFunc = this.logWarning.bind(this);
     this._localIdp = new PeerConnectionIdp(this._win, idpTimeout, warningFunc,
                                            this.dispatchEvent.bind(this));
     this._remoteIdp = new PeerConnectionIdp(this._win, idpTimeout, warningFunc,
                                             this.dispatchEvent.bind(this));
@@ -496,36 +481,36 @@ RTCPeerConnection.prototype = {
   },
 
   // Ideally, this should be of the form _checkState(state),
   // where the state is taken from an enumeration containing
   // the valid peer connection states defined in the WebRTC
   // spec. See Bug 831756.
   _checkClosed: function() {
     if (this._closed) {
-      throw new this._win.DOMError("", "Peer connection is closed");
+      throw new this._win.DOMError("InvalidStateError", "Peer connection is closed");
     }
   },
 
   dispatchEvent: function(event) {
     // PC can close while events are firing if there is an async dispatch
     // in c++ land
     if (!this._closed) {
       this.__DOM_IMPL__.dispatchEvent(event);
     }
   },
 
   // Log error message to web console and window.onerror, if present.
-  logErrorAndCallOnError: function(msg, file, line) {
-    this.logMsg(msg, file, line, Ci.nsIScriptError.exceptionFlag);
+  logErrorAndCallOnError: function(e) {
+    this.logMsg(e.message, e.fileName, e.lineNumber, Ci.nsIScriptError.exceptionFlag);
 
     // Safely call onerror directly if present (necessary for testing)
     try {
       if (typeof this._win.onerror === "function") {
-        this._win.onerror(msg, file, line);
+        this._win.onerror(e.message, e.fileName, e.lineNumber);
       }
     } catch(e) {
       // If onerror itself throws, service it.
       try {
         this.logError(e.message, e.fileName, e.lineNumber);
       } catch(e) {}
     }
   },
@@ -559,23 +544,40 @@ RTCPeerConnection.prototype = {
   makeGetterSetterEH: function(name) {
     Object.defineProperty(this, name,
                           {
                             get:function()  { return this.getEH(name); },
                             set:function(h) { return this.setEH(name, h); }
                           });
   },
 
-  createOffer: function(onSuccess, onError, options) {
+  // Helper for legacy callbacks
+  thenCB: function(p, onSuccess, onError) {
+    var errorFunc = this.logErrorAndCallOnError.bind(this);
+
+    function callCB(func, arg) {
+      try {
+        func(arg);
+      } catch (e) {
+        errorFunc(e);
+      }
+    }
+    return onSuccess? p.then(result => callCB(onSuccess, result),
+                             reason => (onError? callCB(onError, reason) : null)) : p;
+  },
+
+  createOffer: function(optionsOrOnSuccess, onError, options) {
 
     // TODO: Remove old constraint-like RTCOptions support soon (Bug 1064223).
     // Note that webidl bindings make o.mandatory implicit but not o.optional.
     function convertLegacyOptions(o) {
-      if (!(Object.keys(o.mandatory).length || o.optional) ||
-          Object.keys(o).length != (o.optional? 2 : 1)) {
+      // Detect (mandatory OR optional) AND no other top-level members.
+      let lcy = ((o.mandatory && Object.keys(o.mandatory).length) || o.optional) &&
+                Object.keys(o).length == (o.mandatory? 1 : 0) + (o.optional? 1 : 0);
+      if (!lcy) {
         return false;
       }
       let old = o.mandatory || {};
       if (o.mandatory) {
         delete o.mandatory;
       }
       if (o.optional) {
         o.optional.forEach(one => {
@@ -595,27 +597,35 @@ RTCPeerConnection.prototype = {
       Object.keys(o).forEach(k => {
         if (o[k] === undefined) {
           delete o[k];
         }
       });
       return true;
     }
 
+    let onSuccess;
+    if (optionsOrOnSuccess && typeof optionsOrOnSuccess === "function") {
+      onSuccess = optionsOrOnSuccess;
+    } else {
+      options = optionsOrOnSuccess;
+      onError = undefined;
+    }
     if (options && convertLegacyOptions(options)) {
       this.logWarning(
           "Mandatory/optional in createOffer options is deprecated! Use " +
           JSON.stringify(options) + " instead (note the case difference)!",
           null, 0);
     }
-    this._queueOrRun({
+    let p = new this._win.Promise((resolve, reject) => this._queueOrRun({
       func: this._createOffer,
-      args: [onSuccess, onError, options],
+      args: [resolve, reject, options],
       wait: true
-    });
+    }));
+    return this.thenCB(p, onSuccess, onError);
   },
 
   _createOffer: function(onSuccess, onError, options) {
     this._onCreateOfferSuccess = onSuccess;
     this._onCreateOfferFailure = onError;
     this._impl.createOffer(options);
   },
 
@@ -635,88 +645,83 @@ RTCPeerConnection.prototype = {
       this._observer.onCreateAnswerError(Ci.IPeerConnection.kInvalidState,
                                          "No outstanding offer");
       return;
     }
     this._impl.createAnswer();
   },
 
   createAnswer: function(onSuccess, onError) {
-    this._queueOrRun({
+    let p = new this._win.Promise((resolve, reject) => this._queueOrRun({
       func: this._createAnswer,
-      args: [onSuccess, onError],
+      args: [resolve, reject],
       wait: true
-    });
+    }));
+    return this.thenCB(p, onSuccess, onError);
   },
 
   setLocalDescription: function(desc, onSuccess, onError) {
-    if (!onSuccess || !onError) {
-      this.logWarning(
-          "setLocalDescription called without success/failure callbacks. This is deprecated, and will be an error in the future.",
-          null, 0);
-    }
-
     this._localType = desc.type;
 
     let type;
     switch (desc.type) {
       case "offer":
         type = Ci.IPeerConnection.kActionOffer;
         break;
       case "answer":
         type = Ci.IPeerConnection.kActionAnswer;
         break;
       case "pranswer":
-        throw new this._win.DOMError("", "pranswer not yet implemented");
+        throw new this._win.DOMError("NotSupportedError", "pranswer not yet implemented");
       default:
-        throw new this._win.DOMError("",
+        throw new this._win.DOMError("InvalidParameterError",
             "Invalid type " + desc.type + " provided to setLocalDescription");
     }
 
-    this._queueOrRun({
+    let p = new this._win.Promise((resolve, reject) => this._queueOrRun({
       func: this._setLocalDescription,
-      args: [type, desc.sdp, onSuccess, onError],
+      args: [type, desc.sdp, resolve, reject],
       wait: true
-    });
+    }));
+    return this.thenCB(p, onSuccess, onError);
   },
 
   _setLocalDescription: function(type, sdp, onSuccess, onError) {
     this._onSetLocalDescriptionSuccess = onSuccess;
     this._onSetLocalDescriptionFailure = onError;
     this._impl.setLocalDescription(type, sdp);
   },
 
   setRemoteDescription: function(desc, onSuccess, onError) {
-    if (!onSuccess || !onError) {
-      this.logWarning(
-          "setRemoteDescription called without success/failure callbacks. This is deprecated, and will be an error in the future.",
-          null, 0);
-    }
     this._remoteType = desc.type;
 
     let type;
     switch (desc.type) {
       case "offer":
         type = Ci.IPeerConnection.kActionOffer;
         break;
       case "answer":
         type = Ci.IPeerConnection.kActionAnswer;
         break;
       case "pranswer":
-        throw new this._win.DOMError("", "pranswer not yet implemented");
+        throw new this._win.DOMError("NotSupportedError", "pranswer not yet implemented");
       default:
-        throw new this._win.DOMError("",
+        throw new this._win.DOMError("InvalidParameterError",
             "Invalid type " + desc.type + " provided to setRemoteDescription");
     }
 
-    this._queueOrRun({
+    // Have to get caller's origin outside of Promise constructor and pass it in
+    let origin = Cu.getWebIDLCallerPrincipal().origin;
+
+    let p = new this._win.Promise((resolve, reject) => this._queueOrRun({
       func: this._setRemoteDescription,
-      args: [type, desc.sdp, onSuccess, onError],
+      args: [type, desc.sdp, origin, resolve, reject],
       wait: true
-    });
+    }));
+    return this.thenCB(p, onSuccess, onError);
   },
 
   /**
    * Takes a result from the IdP and checks it against expectations.
    * If OK, generates events.
    * Returns true if it is either present and valid, or if there is no
    * need for identity.
    */
@@ -731,32 +736,32 @@ RTCPeerConnection.prototype = {
       this._impl.peerIdentity = message.identity;
       this._peerIdentity = new this._win.RTCIdentityAssertion(
         this._remoteIdp.provider, message.identity);
       this.dispatchEvent(new this._win.Event("peeridentity"));
     }
     return good;
   },
 
-  _setRemoteDescription: function(type, sdp, onSuccess, onError) {
+  _setRemoteDescription: function(type, sdp, origin, onSuccess, onError) {
     let idpComplete = false;
     let setRemoteComplete = false;
     let idpError = null;
     let isDone = false;
 
     // we can run the IdP validation in parallel with setRemoteDescription this
     // complicates much more than would be ideal, but it ensures that the IdP
     // doesn't hold things up too much when it's not on the critical path
     let allDone = () => {
       if (!setRemoteComplete || !idpComplete || isDone) {
         return;
       }
       // May be null if the user didn't supply success/failure callbacks.
       // Violation of spec, but we allow it for now
-      this.callCB(onSuccess);
+      onSuccess();
       isDone = true;
       this._executeNext();
     };
 
     let setRemoteDone = () => {
       setRemoteComplete = true;
       allDone();
     };
@@ -770,27 +775,27 @@ RTCPeerConnection.prototype = {
     } else {
       idpDone = message => {
         let idpGood = this._processIdpResult(message);
         if (!idpGood) {
           // iff we are waiting for a very specific peerIdentity
           // call the error callback directly and then close
           idpError = "Peer Identity mismatch, expected: " +
             this._impl.peerIdentity;
-          this.callCB(onError, idpError);
+          onError(idpError);
           this.close();
         } else {
           idpComplete = true;
           allDone();
         }
       };
     }
 
     try {
-      this._remoteIdp.verifyIdentityFromSDP(sdp, idpDone);
+      this._remoteIdp.verifyIdentityFromSDP(sdp, origin, idpDone);
     } catch (e) {
       // if processing the SDP for identity doesn't work
       this.logWarning(e.message, e.fileName, e.lineNumber);
       idpDone(null);
     }
 
     this._onSetRemoteDescriptionSuccess = setRemoteDone;
     this._onSetRemoteDescriptionFailure = onError;
@@ -817,35 +822,31 @@ RTCPeerConnection.prototype = {
       }
     };
 
     this._localIdp.getIdentityAssertion(this._impl.fingerprint,
                                         gotAssertion);
   },
 
   updateIce: function(config) {
-    throw new this._win.DOMError("", "updateIce not yet implemented");
+    throw new this._win.DOMError("NotSupportedError", "updateIce not yet implemented");
   },
 
   addIceCandidate: function(cand, onSuccess, onError) {
-    if (!onSuccess || !onError) {
-      this.logWarning(
-          "addIceCandidate called without success/failure callbacks. This is deprecated, and will be an error in the future.",
-          null, 0);
-    }
     if (!cand.candidate && !cand.sdpMLineIndex) {
-      throw new this._win.DOMError("",
+      throw new this._win.DOMError("InvalidParameterError",
           "Invalid candidate passed to addIceCandidate!");
     }
 
-    this._queueOrRun({
+    let p = new this._win.Promise((resolve, reject) => this._queueOrRun({
       func: this._addIceCandidate,
-      args: [cand, onSuccess, onError],
+      args: [cand, resolve, reject],
       wait: false
-    });
+    }));
+    return this.thenCB(p, onSuccess, onError);
   },
 
   _addIceCandidate: function(cand, onSuccess, onError) {
     this._onAddIceCandidateSuccess = onSuccess || null;
     this._onAddIceCandidateError = onError || null;
 
     this._impl.addIceCandidate(cand.candidate, cand.sdpMid || "",
                                (cand.sdpMLineIndex === null) ? 0 :
@@ -853,42 +854,42 @@ RTCPeerConnection.prototype = {
   },
 
   addStream: function(stream) {
     stream.getTracks().forEach(track => this.addTrack(track, stream));
   },
 
   removeStream: function(stream) {
      // Bug 844295: Not implementing this functionality.
-     throw new this._win.DOMError("", "removeStream not yet implemented");
+     throw new this._win.DOMError("NotSupportedError", "removeStream not yet implemented");
   },
 
   getStreamById: function(id) {
-    throw new this._win.DOMError("", "getStreamById not yet implemented");
+    throw new this._win.DOMError("NotSupportedError", "getStreamById not yet implemented");
   },
 
   addTrack: function(track, stream) {
     if (stream.currentTime === undefined) {
-      throw new this._win.DOMError("", "invalid stream.");
+      throw new this._win.DOMError("InvalidParameterError", "invalid stream.");
     }
     if (stream.getTracks().indexOf(track) == -1) {
-      throw new this._win.DOMError("", "track is not in stream.");
+      throw new this._win.DOMError("InvalidParameterError", "track is not in stream.");
     }
     this._checkClosed();
     this._impl.addTrack(track, stream);
     let sender = this._win.RTCRtpSender._create(this._win,
                                                 new RTCRtpSender(this, track,
                                                                  stream));
     this._senders.push({ sender: sender, stream: stream });
     return sender;
   },
 
   removeTrack: function(sender) {
      // Bug 844295: Not implementing this functionality.
-     throw new this._win.DOMError("", "removeTrack not yet implemented");
+     throw new this._win.DOMError("NotSupportedError", "removeTrack not yet implemented");
   },
 
   _replaceTrack: function(sender, withTrack, onSuccess, onError) {
     // TODO: Do a (sender._stream.getTracks().indexOf(track) == -1) check
     //       on both track args someday.
     //
     // The proposed API will be that both tracks must already be in the same
     // stream. However, since our MediaStreams currently are limited to one
@@ -1012,21 +1013,22 @@ RTCPeerConnection.prototype = {
 
   changeIceConnectionState: function(state) {
     this._iceConnectionState = state;
     _globalPCList.notifyLifecycleObservers(this, "iceconnectionstatechange");
     this.dispatchEvent(new this._win.Event("iceconnectionstatechange"));
   },
 
   getStats: function(selector, onSuccess, onError) {
-    this._queueOrRun({
+    let p = new this._win.Promise((resolve, reject) => this._queueOrRun({
       func: this._getStats,
-      args: [selector, onSuccess, onError],
+      args: [selector, resolve, reject],
       wait: false
-    });
+    }));
+    return this.thenCB(p, onSuccess, onError);
   },
 
   _getStats: function(selector, onSuccess, onError) {
     this._onGetStatsSuccess = onSuccess;
     this._onGetStatsFailure = onError;
 
     this._impl.getStats(selector);
   },
@@ -1051,17 +1053,17 @@ RTCPeerConnection.prototype = {
     }
     if (dict.stream != undefined) {
       dict.id = dict.stream;
       this.logWarning("Deprecated RTCDataChannelInit dictionary entry stream used!", null, 0);
     }
 
     if (dict.maxRetransmitTime != undefined &&
         dict.maxRetransmits != undefined) {
-      throw new this._win.DOMError("",
+      throw new this._win.DOMError("InvalidParameterError",
           "Both maxRetransmitTime and maxRetransmits cannot be provided");
     }
     let protocol;
     if (dict.protocol == undefined) {
       protocol = "";
     } else {
       protocol = dict.protocol;
     }
@@ -1137,84 +1139,80 @@ PeerConnectionObserver.prototype = {
 
   dispatchEvent: function(event) {
     this._dompc.dispatchEvent(event);
   },
 
   onCreateOfferSuccess: function(sdp) {
     let pc = this._dompc;
     let fp = pc._impl.fingerprint;
-    pc._localIdp.appendIdentityToSDP(sdp, fp, function(sdp, assertion) {
+    let origin = Cu.getWebIDLCallerPrincipal().origin;
+    pc._localIdp.appendIdentityToSDP(sdp, fp, origin, function(sdp, assertion) {
       if (assertion) {
         pc._gotIdentityAssertion(assertion);
       }
-      pc.callCB(pc._onCreateOfferSuccess,
-                new pc._win.mozRTCSessionDescription({ type: "offer",
-                                                       sdp: sdp }));
+      pc._onCreateOfferSuccess(new pc._win.mozRTCSessionDescription({ type: "offer",
+                                                                      sdp: sdp }));
       pc._executeNext();
     }.bind(this));
   },
 
   onCreateOfferError: function(code, message) {
-    this._dompc.callCB(this._dompc._onCreateOfferFailure, this.newError(code, message));
+    this._dompc._onCreateOfferFailure(this.newError(code, message));
     this._dompc._executeNext();
   },
 
   onCreateAnswerSuccess: function(sdp) {
     let pc = this._dompc;
     let fp = pc._impl.fingerprint;
-    pc._localIdp.appendIdentityToSDP(sdp, fp, function(sdp, assertion) {
+    let origin = Cu.getWebIDLCallerPrincipal().origin;
+    pc._localIdp.appendIdentityToSDP(sdp, fp, origin, function(sdp, assertion) {
       if (assertion) {
         pc._gotIdentityAssertion(assertion);
       }
-      pc.callCB(pc._onCreateAnswerSuccess,
-                new pc._win.mozRTCSessionDescription({ type: "answer",
-                                                       sdp: sdp }));
+      pc._onCreateAnswerSuccess(new pc._win.mozRTCSessionDescription({ type: "answer",
+                                                                       sdp: sdp }));
       pc._executeNext();
     }.bind(this));
   },
 
   onCreateAnswerError: function(code, message) {
-    this._dompc.callCB(this._dompc._onCreateAnswerFailure,
-                       this.newError(code, message));
+    this._dompc._onCreateAnswerFailure(this.newError(code, message));
     this._dompc._executeNext();
   },
 
   onSetLocalDescriptionSuccess: function() {
-    this._dompc.callCB(this._dompc._onSetLocalDescriptionSuccess);
+    this._dompc._onSetLocalDescriptionSuccess();
     this._dompc._executeNext();
   },
 
   onSetRemoteDescriptionSuccess: function() {
     // This function calls _executeNext() for us
     this._dompc._onSetRemoteDescriptionSuccess();
   },
 
   onSetLocalDescriptionError: function(code, message) {
     this._localType = null;
-    this._dompc.callCB(this._dompc._onSetLocalDescriptionFailure,
-                       this.newError(code, message));
+    this._dompc._onSetLocalDescriptionFailure(this.newError(code, message));
     this._dompc._executeNext();
   },
 
   onSetRemoteDescriptionError: function(code, message) {
     this._remoteType = null;
-    this._dompc.callCB(this._dompc._onSetRemoteDescriptionFailure,
-                       this.newError(code, message));
+    this._dompc._onSetRemoteDescriptionFailure(this.newError(code, message));
     this._dompc._executeNext();
   },
 
   onAddIceCandidateSuccess: function() {
-    this._dompc.callCB(this._dompc._onAddIceCandidateSuccess);
+    this._dompc._onAddIceCandidateSuccess();
     this._dompc._executeNext();
   },
 
   onAddIceCandidateError: function(code, message) {
-    this._dompc.callCB(this._dompc._onAddIceCandidateError,
-                       this.newError(code, message));
+    this._dompc._onAddIceCandidateError(this.newError(code, message));
     this._dompc._executeNext();
   },
 
   onIceCandidate: function(level, mid, candidate) {
     if (candidate == "") {
       this.foundIceCandidate(null);
     } else {
       this.foundIceCandidate(new this._dompc._win.mozRTCIceCandidate(
@@ -1329,61 +1327,60 @@ PeerConnectionObserver.prototype = {
     }
   },
 
   onGetStatsSuccess: function(dict) {
     let chromeobj = new RTCStatsReport(this._dompc._win, dict);
     let webidlobj = this._dompc._win.RTCStatsReport._create(this._dompc._win,
                                                             chromeobj);
     chromeobj.makeStatsPublic();
-    this._dompc.callCB(this._dompc._onGetStatsSuccess, webidlobj);
+    this._dompc._onGetStatsSuccess(webidlobj);
     this._dompc._executeNext();
   },
 
   onGetStatsError: function(code, message) {
-    this._dompc.callCB(this._dompc._onGetStatsFailure,
-                       this.newError(code, message));
+    this._dompc._onGetStatsFailure(this.newError(code, message));
     this._dompc._executeNext();
   },
 
   onAddStream: function(stream) {
     let ev = new this._dompc._win.MediaStreamEvent("addstream",
                                                    { stream: stream });
-    this._dompc.dispatchEvent(ev);
+    this.dispatchEvent(ev);
   },
 
   onRemoveStream: function(stream, type) {
     this.dispatchEvent(new this._dompc._win.MediaStreamEvent("removestream",
                                                              { stream: stream }));
   },
 
   onAddTrack: function(track) {
     let ev = new this._dompc._win.MediaStreamTrackEvent("addtrack",
                                                         { track: track });
-    this._dompc.dispatchEvent(ev);
+    this.dispatchEvent(ev);
   },
 
   onRemoveTrack: function(track, type) {
     this.dispatchEvent(new this._dompc._win.MediaStreamTrackEvent("removetrack",
                                                                   { track: track }));
   },
 
   onReplaceTrackSuccess: function() {
     var pc = this._dompc;
     pc._onReplaceTrackSender.track = pc._onReplaceTrackWithTrack;
     pc._onReplaceTrackWithTrack = null;
     pc._onReplaceTrackSender = null;
-    pc.callCB(pc._onReplaceTrackSuccess);
+    pc._onReplaceTrackSuccess();
   },
 
   onReplaceTrackError: function(code, message) {
     var pc = this._dompc;
     pc._onReplaceTrackWithTrack = null;
     pc._onReplaceTrackSender = null;
-    pc.callCB(pc._onReplaceTrackError, this.newError(code, message));
+    pc._onReplaceTrackError(this.newError(code, message));
   },
 
   foundIceCandidate: function(cand) {
     this.dispatchEvent(new this._dompc._win.RTCPeerConnectionIceEvent("icecandidate",
                                                                       { candidate: cand } ));
   },
 
   notifyDataChannel: function(channel) {
@@ -1418,23 +1415,24 @@ function RTCRtpSender(pc, track, stream)
   this._stream = stream;
 }
 RTCRtpSender.prototype = {
   classDescription: "RTCRtpSender",
   classID: PC_SENDER_CID,
   contractID: PC_SENDER_CONTRACT,
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]),
 
-  replaceTrack: function(withTrack, onSuccess, onError) {
+  replaceTrack: function(withTrack) {
     this._pc._checkClosed();
-    this._pc._queueOrRun({
+
+    return new this._pc._win.Promise((resolve, reject) => this._pc._queueOrRun({
       func: this._pc._replaceTrack,
-      args: [this, withTrack, onSuccess, onError],
+      args: [this, withTrack, resolve, reject],
       wait: false
-    });
+    }));
   }
 };
 
 function RTCRtpReceiver(pc, track) {
   this.pc = pc;
   this.track = track;
 }
 RTCRtpReceiver.prototype = {
--- a/dom/media/PeerConnectionIdp.jsm
+++ b/dom/media/PeerConnectionIdp.jsm
@@ -119,28 +119,28 @@ PeerConnectionIdp.prototype = {
   },
 
   /**
    * Queues a task to verify the a=identity line the given SDP contains, if any.
    * If the verification succeeds callback is called with the message from the
    * IdP proxy as parameter, else (verification failed OR no a=identity line in
    * SDP at all) null is passed to callback.
    */
-  verifyIdentityFromSDP: function(sdp, callback) {
+  verifyIdentityFromSDP: function(sdp, origin, callback) {
     let identity = this._getIdentityFromSdp(sdp);
     let fingerprints = this._getFingerprintsFromSdp(sdp);
     // it's safe to use the fingerprint we got from the SDP here,
     // only because we ensure that there is only one
     if (!identity || fingerprints.length <= 0) {
       callback(null);
       return;
     }
 
     this.setIdentityProvider(identity.idp.domain, identity.idp.protocol);
-    this._verifyIdentity(identity.assertion, fingerprints, callback);
+    this._verifyIdentity(identity.assertion, fingerprints, origin, callback);
   },
 
   /**
    * Checks that the name in the identity provided by the IdP is OK.
    *
    * @param name (string) the name to validate
    * @returns (string) an error message, iff the name isn't good
    */
@@ -207,50 +207,51 @@ PeerConnectionIdp.prototype = {
       warn("invalid JSON in content");
     }
     return false;
   },
 
   /**
    * Asks the IdP proxy to verify an identity.
    */
-  _verifyIdentity: function(assertion, fingerprints, callback) {
+  _verifyIdentity: function(assertion, fingerprints, origin, callback) {
     function onVerification(message) {
       if (message && this._checkVerifyResponse(message, fingerprints)) {
         callback(message);
       } else {
         this._warning("RTC identity: assertion validation failure", null, 0);
         callback(null);
       }
     }
 
     let request = {
       type: "VERIFY",
-      message: assertion
+      message: assertion,
+      origin: origin
     };
     this._sendToIdp(request, "validation", onVerification.bind(this));
   },
 
   /**
    * Asks the IdP proxy for an identity assertion and, on success, enriches the
    * given SDP with an a=identity line and calls callback with the new SDP as
    * parameter. If no IdP is configured the original SDP (without a=identity
    * line) is passed to the callback.
    */
-  appendIdentityToSDP: function(sdp, fingerprint, callback) {
+  appendIdentityToSDP: function(sdp, fingerprint, origin, callback) {
     let onAssertion = function() {
       callback(this.wrapSdp(sdp), this.assertion);
     }.bind(this);
 
     if (!this._idpchannel || this.assertion) {
       onAssertion();
       return;
     }
 
-    this._getIdentityAssertion(fingerprint, onAssertion);
+    this._getIdentityAssertion(fingerprint, origin, onAssertion);
   },
 
   /**
    * Inserts an identity assertion into the given SDP.
    */
   wrapSdp: function(sdp) {
     if (!this.assertion) {
       return sdp;
@@ -265,31 +266,33 @@ PeerConnectionIdp.prototype = {
 
   getIdentityAssertion: function(fingerprint, callback) {
     if (!this._idpchannel) {
       this.reportError("assertion", "IdP not set");
       callback(null);
       return;
     }
 
-    this._getIdentityAssertion(fingerprint, callback);
+    let origin = Cu.getWebIDLCallerPrincipal().origin;
+    this._getIdentityAssertion(fingerprint, origin, callback);
   },
 
-  _getIdentityAssertion: function(fingerprint, callback) {
+  _getIdentityAssertion: function(fingerprint, origin, callback) {
     let [algorithm, digest] = fingerprint.split(" ", 2);
     let message = {
       fingerprint: [{
         algorithm: algorithm,
         digest: digest
       }]
     };
     let request = {
       type: "SIGN",
       message: JSON.stringify(message),
-      username: this.username
+      username: this.username,
+      origin: origin
     };
 
     // catch the assertion, clean it up, warn if absent
     function trapAssertion(assertion) {
       if (!assertion) {
         this._warning("RTC identity: assertion generation failure", null, 0);
         this.assertion = null;
       } else {
@@ -303,17 +306,16 @@ PeerConnectionIdp.prototype = {
 
   /**
    * Packages a message and sends it to the IdP.
    * @param request (dictionary) the message to send
    * @param type (DOMString) the type of message (assertion/validation)
    * @param callback (function) the function to call with the results
    */
   _sendToIdp: function(request, type, callback) {
-    request.origin = Cu.getWebIDLCallerPrincipal().origin;
     this._idpchannel.send(request, this._wrapCallback(type, callback));
   },
 
   _reportIdpError: function(type, message) {
     let args = {};
     let msg = "";
     if (message.type === "ERROR") {
       msg = message.error;
--- a/dom/media/tests/mochitest/mochitest.ini
+++ b/dom/media/tests/mochitest/mochitest.ini
@@ -78,18 +78,16 @@ skip-if = buildapp == 'b2g' || os == 'an
 [test_peerConnection_bug822674.html]
 skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
 [test_peerConnection_bug825703.html]
 skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
 [test_peerConnection_bug827843.html]
 skip-if = toolkit == 'gonk' # b2g(Bug 960442, video support for WebRTC is disabled on b2g)
 [test_peerConnection_bug834153.html]
 skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
-[test_peerConnection_bug835370.html]
-skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
 [test_peerConnection_bug1013809.html]
 skip-if = toolkit == 'gonk' # b2g emulator seems to be too slow (Bug 1016498 and 1008080)
 [test_peerConnection_bug1042791.html]
 skip-if = buildapp == 'b2g' || os == 'android' # bug 1043403
 [test_peerConnection_capturedVideo.html]
 skip-if = toolkit == 'gonk' # b2g(Bug 960442, video support for WebRTC is disabled on b2g)
 [test_peerConnection_close.html]
 skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
@@ -102,16 +100,18 @@ skip-if = toolkit == 'gonk' # b2g (Bug 1
 [test_peerConnection_noTrickleOfferAnswer.html]
 skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
 [test_peerConnection_offerRequiresReceiveAudio.html]
 skip-if = toolkit == 'gonk' # b2g(Bug 960442, video support for WebRTC is disabled on b2g)
 [test_peerConnection_offerRequiresReceiveVideo.html]
 skip-if = toolkit == 'gonk' # b2g(Bug 960442, video support for WebRTC is disabled on b2g)
 [test_peerConnection_offerRequiresReceiveVideoAudio.html]
 skip-if = toolkit == 'gonk' # b2g(Bug 960442, video support for WebRTC is disabled on b2g)
+[test_peerConnection_promiseSendOnly.html]
+skip-if = toolkit == 'gonk' # b2g(Bug 960442, video support for WebRTC is disabled on b2g)
 [test_peerConnection_replaceTrack.html]
 skip-if = toolkit == 'gonk' # b2g(Bug 960442, video support for WebRTC is disabled on b2g)
 [test_peerConnection_syncSetDescription.html]
 skip-if = toolkit == 'gonk' # b2g(Bug 960442, video support for WebRTC is disabled on b2g)
 [test_peerConnection_setLocalAnswerInHaveLocalOffer.html]
 skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
 [test_peerConnection_setLocalAnswerInStable.html]
 skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
--- a/dom/media/tests/mochitest/test_dataChannel_noOffer.html
+++ b/dom/media/tests/mochitest/test_dataChannel_noOffer.html
@@ -15,20 +15,21 @@
   });
 
   runNetworkTest(function () {
     var pc = new mozRTCPeerConnection();
 
     // necessary to circumvent bug 864109
     var options = { offerToReceiveAudio: true };
 
-    pc.createOffer(function (offer) {
+    pc.createOffer(options).then(offer => {
       ok(!offer.sdp.contains("m=application"),
         "m=application is not contained in the SDP");
 
       networkTestFinished();
-    }, generateErrorCallback(), options);
+    })
+    .catch(generateErrorCallback());
   });
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_peerConnection_bug834153.html
+++ b/dom/media/tests/mochitest/test_peerConnection_bug834153.html
@@ -9,41 +9,32 @@
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "834153",
     title: "Queue CreateAnswer in PeerConnection.js"
   });
 
-  function croak(msg) {
-    ok(0, msg);
-    pc1.close();
-    pc2.close();
-    networkTestFinished();
-  }
-
   runNetworkTest(function () {
     var pc1 = new mozRTCPeerConnection();
+    var pc2 = new mozRTCPeerConnection();
 
-    pc1.createOffer(function (d) {
-      var pc2 = new mozRTCPeerConnection();
-
+    pc1.createOffer({ offerToReceiveAudio: true }).then(offer => {
       // The whole point of this test is not to wait for the
       // setRemoteDescription call to succesfully complete, so we
-      // don't do anything in its callbacks.
-      pc2.setRemoteDescription(d, function (x) {}, function (x) {});
-      pc2.createAnswer(function (d) {
-        is(d.type,"answer","CreateAnswer created an answer");
-        pc1.close();
-        pc2.close();
-        networkTestFinished();
-      }, function (err) {
-        croak("createAnswer failed: " + err);
-      });
-    }, function (err) {
-        croak("createOffer failed: " + err);
-    },  { offerToReceiveAudio: true });
+      // don't wait for it to succeed.
+      pc2.setRemoteDescription(offer);
+      return pc2.createAnswer();
+    })
+    .then(answer => is(answer.type, "answer", "CreateAnswer created an answer"))
+    .catch(reason => ok(false, reason.message))
+    .then(() => {
+      pc1.close();
+      pc2.close();
+      networkTestFinished();
+    })
+    .catch(reason => ok(false, reason.message));
   });
 </script>
 </pre>
 </body>
 </html>
deleted file mode 100644
--- a/dom/media/tests/mochitest/test_peerConnection_bug835370.html
+++ /dev/null
@@ -1,53 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
-  <script type="application/javascript" src="pc.js"></script>
-</head>
-<body>
-<pre id="test">
-<script type="application/javascript">
-  createHTML({
-    bug: "835370",
-    title: "PeerConnection.createOffer valid/invalid constraints permutations"
-  });
-
-  runNetworkTest(function () {
-    var pconnect  = new mozRTCPeerConnection();
-    var pconnects = new mozRTCPeerConnection();
-
-    function step1(offer) {}
-    function failed(code) {}
-
-    var exception = null;
-    try { pconnects.createOffer(step1, failed); } catch (e) { exception = e; }
-    ok(!exception, "createOffer(step1, failed) succeeds");
-    exception = null;
-    try { pconnect.createOffer(step1, failed, 1); } catch (e) { exception = e; }
-    ok(exception, "createOffer(step1, failed, 1) throws");
-    exception = null;
-    try { pconnects.createOffer(step1, failed, {}); } catch (e) { exception = e; }
-    ok(!exception, "createOffer(step1, failed, {}) succeeds");
-    exception = null;
-    try {
-        pconnect.updateIce();
-    } catch (e) {
-        ok(e.message.indexOf("updateIce") >= 0, "PeerConnection.js has readable exceptions");
-        exception = e;
-    }
-    ok(exception, "updateIce not yet implemented and throws");
-    exception = null;
-    try { pconnects.createOffer(step1, failed, { offerToReceiveVideo: false, offerToReceiveAudio: true, MozDontOfferDataChannel: true }); } catch (e) { exception = e; }
-    ok(!exception, "createOffer(step1, failed, { offerToReceiveVideo: false, offerToReceiveAudio: true, MozDontOfferDataChannel: true }) succeeds");
-    pconnect.close();
-    pconnects.close();
-    pconnect = null;
-    pconnects = null;
-    networkTestFinished();
-  });
-</script>
-</pre>
-</body>
-</html>
--- a/dom/media/tests/mochitest/test_peerConnection_close.html
+++ b/dom/media/tests/mochitest/test_peerConnection_close.html
@@ -20,49 +20,82 @@
     var eTimeout = null;
 
     // everything should be in initial state
     is(pc.signalingState, "stable", "Initial signalingState is 'stable'");
     is(pc.iceConnectionState, "new", "Initial iceConnectionState is 'new'");
     is(pc.iceGatheringState, "new", "Initial iceGatheringState is 'new'");
 
     var finish;
-    var finished = new Promise(function(resolve) {
-      finish = resolve;
-    });
+    var finished = new Promise(resolve => finish = resolve);
 
     pc.onsignalingstatechange = function(e) {
       clearTimeout(eTimeout);
       is(pc.signalingState, "closed", "signalingState is 'closed'");
       is(pc.iceConnectionState, "closed", "iceConnectionState is 'closed'");
 
       try {
         pc.close();
       } catch (e) {
         exception = e;
       }
       is(exception, null, "A second close() should not raise an exception");
       is(pc.signalingState, "closed", "Final signalingState stays at 'closed'");
       is(pc.iceConnectionState, "closed", "Final iceConnectionState stays at 'closed'");
 
-      // TODO: according to the webrtc spec all of these should throw InvalidStateError's
-      //       instead they seem to throw simple Error's
-      SimpleTest.doesThrow(function() {
-        pc.setLocalDescription(
-          "Invalid Session Description",
-          function() {},
-          function() {})},
-        "setLocalDescription() on closed PC raised expected exception");
+      // Due to a limitation in our WebIDL compiler that prevents overloads with
+      // both Promise and non-Promise return types, legacy APIs with callbacks
+      // are unable to continue to throw exceptions. Luckily the spec uses
+      // exceptions solely for "programming errors" so this should not hinder
+      // working code from working, which is the point of the legacy API. All
+      // new code should use the promise API.
+      //
+      // The legacy methods that no longer throw on programming errors like
+      // "invalid-on-close" are:
+      // - createOffer
+      // - createAnswer
+      // - setLocalDescription
+      // - setRemoteDescription
+      // - addIceCandidate
+      // - getStats
+      //
+      // These legacy methods fire the error callback instead. This is not
+      // entirely to spec but is better than ignoring programming errors.
+
+      var offer = new mozRTCSessionDescription({ sdp: "sdp", type: "offer" });
+      var answer = new mozRTCSessionDescription({ sdp: "sdp", type: "answer" });
+      var candidate = new mozRTCIceCandidate({ candidate: "dummy",
+                                               sdpMid: "test",
+                                               sdpMLineIndex: 3 });
 
-      SimpleTest.doesThrow(function() {
-        pc.setRemoteDescription(
-          "Invalid Session Description",
-          function() {},
-          function() {})},
-        "setRemoteDescription() on closed PC raised expected exception");
+      var doesFail = (p, msg) => p.then(generateErrorCallback(),
+                                        r => is(r.name, "InvalidStateError", msg));
+
+      doesFail(pc.createOffer(), "createOffer fails on close")
+      .then(() => doesFail(pc.createAnswer(), "createAnswer fails on close"))
+      .then(() => doesFail(pc.setLocalDescription(offer),
+                           "setLocalDescription fails on close"))
+      .then(() => doesFail(pc.setRemoteDescription(answer),
+                           "setRemoteDescription fails on close"))
+      .then(() => doesFail(pc.addIceCandidate(candidate),
+                           "addIceCandidate fails on close"))
+      .then(() => doesFail(new Promise((y, n) => pc.createOffer(y, n)),
+                           "Legacy createOffer fails on close"))
+      .then(() => doesFail(new Promise((y, n) => pc.createAnswer(y, n)),
+                           "Legacy createAnswer fails on close"))
+      .then(() => doesFail(new Promise((y, n) => pc.setLocalDescription(offer, y, n)),
+                           "Legacy setLocalDescription fails on close"))
+      .then(() => doesFail(new Promise((y, n) => pc.setRemoteDescription(answer, y, n)),
+                           "Legacy setRemoteDescription fails on close"))
+      .then(() => doesFail(new Promise((y, n) => pc.addIceCandidate(candidate, y, n)),
+                           "Legacy addIceCandidate fails on close"))
+      .catch(reason => ok(false, "unexpected failure: " + reason))
+      .then(finish);
+
+      // Other methods are unaffected.
 
       SimpleTest.doesThrow(function() {
         pc.updateIce("Invalid RTC Configuration")},
         "updateIce() on closed PC raised expected exception");
 
       SimpleTest.doesThrow(function() {
         pc.addStream("Invalid Media Stream")},
         "addStream() on closed PC raised expected exception");
@@ -70,27 +103,20 @@
       SimpleTest.doesThrow(function() {
         pc.removeStream("Invalid Media Stream")},
         "removeStream() on closed PC raised expected exception");
 
       SimpleTest.doesThrow(function() {
         pc.createDataChannel({})},
         "createDataChannel() on closed PC raised expected exception");
 
-      // The spec says it has to throw, but it seems questionable why...
-      SimpleTest.doesThrow(function() {
-        pc.getStats()},
-        "getStats() on closed PC raised expected exception");
-
       SimpleTest.doesThrow(function() {
         pc.setIdentityProvider("Invalid Provider")},
         "setIdentityProvider() on closed PC raised expected exception");
-
-      finish();
-    }
+    };
 
     // This prevents a mochitest timeout in case the event does not fire
     eTimeout = setTimeout(function() {
       ok(false, "Failed to receive expected onsignalingstatechange event in 60s");
       finish();
     }, 60000);
 
     try {
--- a/dom/media/tests/mochitest/test_peerConnection_errorCallbacks.html
+++ b/dom/media/tests/mochitest/test_peerConnection_errorCallbacks.html
@@ -9,62 +9,65 @@
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "834270",
     title: "Align PeerConnection error handling with WebRTC specification"
   });
 
-  function errorCallback(nextStep) {
-    return function (err) {
-      ok(err, "Error is set");
-      ok(err.name && err.name.length, "Error name = " + err.name);
-      ok(err.message && err.message.length, "Error message = " + err.message);
-      nextStep();
-    }
+  function validateReason(reason) {
+    ok(reason.name.length, "Reason name = " + reason.name);
+    ok(reason.message.length, "Reason message = " + reason.message);
   };
 
   function testCreateAnswerError() {
     var pc = new mozRTCPeerConnection();
-    info ("Testing createAnswer error callback");
-    pc.createAnswer(generateErrorCallback("createAnswer before offer should fail"),
-                    errorCallback(testSetLocalDescriptionError));
+    info ("Testing createAnswer error");
+    return pc.createAnswer()
+    .then(generateErrorCallback("createAnswer before offer should fail"),
+          validateReason);
   };
 
   function testSetLocalDescriptionError() {
     var pc = new mozRTCPeerConnection();
-    info ("Testing setLocalDescription error callback");
-    pc.setLocalDescription(new mozRTCSessionDescription({ sdp: "Picklechips!",
-                                                          type: "offer" }),
-      generateErrorCallback("setLocalDescription with nonsense SDP should fail"),
-      errorCallback(testSetRemoteDescriptionError));
+    info ("Testing setLocalDescription error");
+    return pc.setLocalDescription(new mozRTCSessionDescription({ sdp: "Picklechips!",
+                                                                 type: "offer" }))
+    .then(generateErrorCallback("setLocalDescription with nonsense SDP should fail"),
+          validateReason);
   };
 
   function testSetRemoteDescriptionError() {
     var pc = new mozRTCPeerConnection();
-    info ("Testing setRemoteDescription error callback");
-    pc.setRemoteDescription(new mozRTCSessionDescription({ sdp: "Who?",
-                                                           type: "offer" }),
-      generateErrorCallback("setRemoteDescription with nonsense SDP should fail"),
-      errorCallback(testAddIceCandidateError));
+    info ("Testing setRemoteDescription error");
+    return pc.setRemoteDescription(new mozRTCSessionDescription({ sdp: "Who?",
+                                                                  type: "offer" }))
+    .then(generateErrorCallback("setRemoteDescription with nonsense SDP should fail"),
+          validateReason);
   };
 
   function testAddIceCandidateError() {
     var pc = new mozRTCPeerConnection();
-    info ("Testing addIceCandidate error callback");
-    pc.addIceCandidate(new mozRTCIceCandidate({ candidate: "Pony Lords, jump!",
-                                                sdpMid: "whee",
-                                                sdpMLineIndex: 1 }),
-      generateErrorCallback("addIceCandidate with nonsense candidate should fail"),
-      errorCallback(networkTestFinished));
+    info ("Testing addIceCandidate error");
+    return pc.addIceCandidate(new mozRTCIceCandidate({ candidate: "Pony Lords, jump!",
+                                                       sdpMid: "whee",
+                                                       sdpMLineIndex: 1 }))
+    .then(generateErrorCallback("addIceCandidate with nonsense candidate should fail"),
+          validateReason);
   };
 
   // No test for createOffer errors -- there's nothing we can do at this
   // level to evoke an error in createOffer.
 
   runNetworkTest(function () {
-    testCreateAnswerError();
+    testCreateAnswerError()
+    .then(testSetLocalDescriptionError)
+    .then(testSetRemoteDescriptionError)
+    .then(testAddIceCandidateError)
+    .catch(reason => ok(false, "unexpected error: " + reason))
+    .then(networkTestFinished);
   });
+
 </script>
 </pre>
 </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_promiseSendOnly.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="head.js"></script>
+  <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<video id="v1" controls="controls" height="120" width="160" autoplay></video>
+<video id="v2" controls="controls" height="120" width="160" autoplay></video><br>
+<pre id="test">
+<script type="application/javascript;version=1.8">
+  createHTML({
+    bug: "1091898",
+    title: "PeerConnection with promises (sendonly)",
+    visible: true
+  });
+
+  var waituntil = func => new Promise(resolve => {
+    var inter = setInterval(() => func() && resolve(clearInterval(inter)), 200);
+  });
+
+  var pc1 = new mozRTCPeerConnection();
+  var pc2 = new mozRTCPeerConnection();
+
+  var pc2_haveRemoteOffer = new Promise(resolve => pc2.onsignalingstatechange =
+    e => (e.target.signalingState == "have-remote-offer") && resolve());
+  var pc1_stable = new Promise(resolve => pc1.onsignalingstatechange =
+    e => (e.target.signalingState == "stable") && resolve());
+
+  pc1.onicecandidate = e => pc2_haveRemoteOffer.then(() => !e.candidate ||
+    pc2.addIceCandidate(e.candidate)).catch(generateErrorCallback());
+  pc2.onicecandidate = e => pc1_stable.then(() => !e.candidate ||
+    pc1.addIceCandidate(e.candidate)).catch(generateErrorCallback());
+
+  var delivered = new Promise(resolve =>
+    pc2.onaddstream = e => resolve(v2.mozSrcObject = e.stream));
+  var canPlayThrough = new Promise(resolve => v2.canplaythrough = e => resolve());
+
+  runNetworkTest(function() {
+    is(v2.currentTime, 0, "v2.currentTime is zero at outset");
+
+    navigator.mediaDevices.getUserMedia({ fake: true, video: true, audio: true })
+    .then(stream => pc1.addStream(v1.mozSrcObject = stream))
+    .then(() => pc1.createOffer())
+    .then(offer => pc1.setLocalDescription(offer))
+    .then(() => pc2.setRemoteDescription(pc1.localDescription))
+    .then(() => pc2.createAnswer())
+    .then(answer => pc2.setLocalDescription(answer))
+    .then(() => pc1.setRemoteDescription(pc2.localDescription))
+    .then(() => delivered)
+//    .then(() => canPlayThrough)    // why doesn't this fire?
+    .then(() => waituntil(() => v2.currentTime > 0 && v2.mozSrcObject.currentTime > 0))
+    .then(() => ok(v2.currentTime > 0, "v2.currentTime is moving (" + v2.currentTime + ")"))
+    .then(() => ok(true, "Connected."))
+    .catch(reason => ok(false, "unexpected failure: " + reason))
+    .then(networkTestFinished);
+  });
+</script>
+</pre>
+</body>
+</html>
--- a/dom/media/tests/mochitest/test_peerConnection_replaceTrack.html
+++ b/dom/media/tests/mochitest/test_peerConnection_replaceTrack.html
@@ -32,27 +32,23 @@
     var flowtest = test.chain.remove("PC_REMOTE_CHECK_MEDIA_FLOW_PRESENT");
     test.chain.append(flowtest);
     test.chain.append([["PC_LOCAL_REPLACE_VIDEOTRACK",
       function (test) {
         var stream = test.pcLocal._pc.getLocalStreams()[0];
         var track = stream.getVideoTracks()[0];
         var sender = test.pcLocal._pc.getSenders().find(isSenderOfTrack, track);
         ok(sender, "track has a sender");
+        var newtrack;
         navigator.mediaDevices.getUserMedia({video:true, fake: true})
         .then(function(newStream) {
-          var newtrack = newStream.getVideoTracks()[0];
-          return new Promise(function(resolve, reject) {
-            sender.replaceTrack(newtrack, function() {
-              resolve(newtrack);
-            }, reject);
-          });
+          newtrack = newStream.getVideoTracks()[0];
+          return sender.replaceTrack(newtrack);
         })
-        .then(function(newtrack) {
-          ok(true, "replaceTrack success callback is called");
+        .then(function() {
           is(sender.track, newtrack, "sender.track has been replaced");
         })
         .catch(function(reason) {
           ok(false, "unexpected error = " + reason.message);
         })
         .then(test.next.bind(test));
       }
     ]]);
--- a/dom/webidl/RTCPeerConnection.webidl
+++ b/dom/webidl/RTCPeerConnection.webidl
@@ -79,34 +79,25 @@ interface RTCDataChannel;
 // moz-prefixed until sufficiently standardized.
 interface mozRTCPeerConnection : EventTarget  {
   [Pref="media.peerconnection.identity.enabled"]
   void setIdentityProvider (DOMString provider,
                             optional DOMString protocol,
                             optional DOMString username);
   [Pref="media.peerconnection.identity.enabled"]
   void getIdentityAssertion();
-  void createOffer (RTCSessionDescriptionCallback successCallback,
-                    RTCPeerConnectionErrorCallback failureCallback,
-                    optional RTCOfferOptions options);
-  void createAnswer (RTCSessionDescriptionCallback successCallback,
-                     RTCPeerConnectionErrorCallback failureCallback);
-  void setLocalDescription (mozRTCSessionDescription description,
-                            optional VoidFunction successCallback,
-                            optional RTCPeerConnectionErrorCallback failureCallback);
-  void setRemoteDescription (mozRTCSessionDescription description,
-                             optional VoidFunction successCallback,
-                             optional RTCPeerConnectionErrorCallback failureCallback);
+  Promise<mozRTCSessionDescription> createOffer (optional RTCOfferOptions options);
+  Promise<mozRTCSessionDescription> createAnswer ();
+  Promise<void> setLocalDescription (mozRTCSessionDescription description);
+  Promise<void> setRemoteDescription (mozRTCSessionDescription description);
   readonly attribute mozRTCSessionDescription? localDescription;
   readonly attribute mozRTCSessionDescription? remoteDescription;
   readonly attribute RTCSignalingState signalingState;
   void updateIce (optional RTCConfiguration configuration);
-  void addIceCandidate (mozRTCIceCandidate candidate,
-                        optional VoidFunction successCallback,
-                        optional RTCPeerConnectionErrorCallback failureCallback);
+  Promise<void> addIceCandidate (mozRTCIceCandidate candidate);
   readonly attribute RTCIceGatheringState iceGatheringState;
   readonly attribute RTCIceConnectionState iceConnectionState;
   [Pref="media.peerconnection.identity.enabled"]
   readonly attribute RTCIdentityAssertion? peerIdentity;
 
   [ChromeOnly]
   attribute DOMString id;
 
@@ -133,26 +124,49 @@ interface mozRTCPeerConnection : EventTa
   attribute EventHandler onnegotiationneeded;
   attribute EventHandler onicecandidate;
   attribute EventHandler onsignalingstatechange;
   attribute EventHandler onaddstream;
   attribute EventHandler onaddtrack;  // replaces onaddstream; see AddTrackEvent
   attribute EventHandler onremovestream;
   attribute EventHandler oniceconnectionstatechange;
 
-  void getStats (MediaStreamTrack? selector,
-                 RTCStatsCallback successCallback,
-                 RTCPeerConnectionErrorCallback failureCallback);
+  Promise<RTCStatsReport> getStats (MediaStreamTrack? selector);
 
   // Data channel.
   RTCDataChannel createDataChannel (DOMString label,
                                     optional RTCDataChannelInit dataChannelDict);
   attribute EventHandler ondatachannel;
   [Pref="media.peerconnection.identity.enabled"]
   attribute EventHandler onidentityresult;
   [Pref="media.peerconnection.identity.enabled"]
   attribute EventHandler onpeeridentity;
   [Pref="media.peerconnection.identity.enabled"]
   attribute EventHandler onidpassertionerror;
   [Pref="media.peerconnection.identity.enabled"]
   attribute EventHandler onidpvalidationerror;
 };
 
+// Legacy callback API
+
+partial interface mozRTCPeerConnection {
+
+  // Dummy Promise<void> return values avoid "WebIDL.WebIDLError: error:
+  // We have overloads with both Promise and non-Promise return types"
+
+  Promise<void> createOffer (RTCSessionDescriptionCallback successCallback,
+                             RTCPeerConnectionErrorCallback failureCallback,
+                             optional RTCOfferOptions options);
+  Promise<void> createAnswer (RTCSessionDescriptionCallback successCallback,
+                              RTCPeerConnectionErrorCallback failureCallback);
+  Promise<void> setLocalDescription (mozRTCSessionDescription description,
+                                     VoidFunction successCallback,
+                                     RTCPeerConnectionErrorCallback failureCallback);
+  Promise<void> setRemoteDescription (mozRTCSessionDescription description,
+                                      VoidFunction successCallback,
+                                      RTCPeerConnectionErrorCallback failureCallback);
+  Promise<void> addIceCandidate (mozRTCIceCandidate candidate,
+                                 VoidFunction successCallback,
+                                 RTCPeerConnectionErrorCallback failureCallback);
+  Promise<void> getStats (MediaStreamTrack? selector,
+                          RTCStatsCallback successCallback,
+                          RTCPeerConnectionErrorCallback failureCallback);
+};
--- a/dom/webidl/RTCRtpSender.webidl
+++ b/dom/webidl/RTCRtpSender.webidl
@@ -7,12 +7,10 @@
  * http://lists.w3.org/Archives/Public/public-webrtc/2014May/0067.html
  */
 
 [Pref="media.peerconnection.enabled",
  JSImplementation="@mozilla.org/dom/rtpsender;1"]
 interface RTCRtpSender {
   readonly attribute MediaStreamTrack track;
 
-  void replaceTrack(MediaStreamTrack track,
-                    VoidFunction successCallback,
-                    RTCPeerConnectionErrorCallback failureCallback);
+  Promise<void> replaceTrack(MediaStreamTrack track);
 };
--- a/js/src/jsapi.cpp
+++ b/js/src/jsapi.cpp
@@ -38,17 +38,16 @@
 #include "jsproxy.h"
 #include "jsscript.h"
 #include "jsstr.h"
 #include "jstypes.h"
 #include "jsutil.h"
 #include "jswatchpoint.h"
 #include "jsweakmap.h"
 #include "jswrapper.h"
-#include "prmjtime.h"
 
 #include "asmjs/AsmJSLink.h"
 #include "builtin/AtomicsObject.h"
 #include "builtin/Eval.h"
 #include "builtin/Intl.h"
 #include "builtin/MapObject.h"
 #include "builtin/RegExp.h"
 #include "builtin/SymbolObject.h"
--- a/js/src/jscompartment.cpp
+++ b/js/src/jscompartment.cpp
@@ -46,16 +46,17 @@ JSCompartment::JSCompartment(Zone *zone,
     isSelfHosting(false),
     marked(true),
     addonId(options.addonIdOrNull()),
 #ifdef DEBUG
     firedOnNewGlobalObject(false),
 #endif
     global_(nullptr),
     enterCompartmentDepth(0),
+    totalTime(0),
     data(nullptr),
     objectMetadataCallback(nullptr),
     lastAnimationTime(0),
     regExps(runtime_),
     globalWriteBarriered(false),
     neuteredTypedObjects(0),
     propertyTree(thisForCtor()),
     selfHostingScriptSource(nullptr),
--- a/js/src/jscompartment.h
+++ b/js/src/jscompartment.h
@@ -4,16 +4,17 @@
  * 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/. */
 
 #ifndef jscompartment_h
 #define jscompartment_h
 
 #include "mozilla/MemoryReporting.h"
 
+#include "prmjtime.h"
 #include "builtin/RegExp.h"
 #include "gc/Zone.h"
 #include "vm/GlobalObject.h"
 #include "vm/PIC.h"
 #include "vm/SavedStacks.h"
 
 namespace js {
 
@@ -161,20 +162,32 @@ struct JSCompartment
 
   private:
     friend struct JSRuntime;
     friend struct JSContext;
     friend class js::ExclusiveContext;
     js::ReadBarrieredGlobalObject global_;
 
     unsigned                     enterCompartmentDepth;
+    int64_t                      startInterval;
 
   public:
-    void enter() { enterCompartmentDepth++; }
-    void leave() { enterCompartmentDepth--; }
+    int64_t                      totalTime;
+    void enter() {
+        if (addonId && !enterCompartmentDepth) {
+            startInterval = PRMJ_Now();
+        }
+        enterCompartmentDepth++;
+    }
+    void leave() {
+        enterCompartmentDepth--;
+        if (addonId && !enterCompartmentDepth) {
+            totalTime += (PRMJ_Now() - startInterval);
+        }
+    }
     bool hasBeenEntered() { return !!enterCompartmentDepth; }
 
     JS::Zone *zone() { return zone_; }
     const JS::Zone *zone() const { return zone_; }
     JS::CompartmentOptions &options() { return options_; }
     const JS::CompartmentOptions &options() const { return options_; }
 
     JSRuntime *runtimeFromMainThread() {
--- a/mobile/android/base/tests/robocop.ini
+++ b/mobile/android/base/tests/robocop.ini
@@ -1,13 +1,14 @@
 [testGeckoProfile]
 # [test_bug720538] # disabled on fig - bug 897072
 [testAboutPage]
 # disabled on Android 2.3; bug 975187
 skip-if = android_version == "10"
+[testAboutPasswords]
 [testAddonManager]
 # disabled on x86; bug 936216
 # disabled on 2.3; bug 941624, bug 1063509, bug 1073374, bug 1087221, bug 1088023, bug 1088027, bug 1090206
 skip-if = android_version == "10" || processor == "x86"
 [testAddSearchEngine]
 # disabled on Android 2.3; bug 979552
 skip-if = android_version == "10"
 [testAdobeFlash]
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tests/testAboutPasswords.java
@@ -0,0 +1,11 @@
+/* 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/. */
+
+package org.mozilla.gecko.tests;
+
+public class testAboutPasswords extends JavascriptTest {
+    public testAboutPasswords() {
+        super("testAboutPasswords.js");
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tests/testAboutPasswords.js
@@ -0,0 +1,101 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict"
+
+const { interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/AndroidLog.jsm");
+
+function ok(passed, text) {
+  do_report_result(passed, text, Components.stack.caller, false);
+}
+
+const LOGIN_FIELDS = {
+  hostname: "http://example.org/tests/robocop/robocop_blank_01.html",
+  formSubmitUrl: "",
+  realmAny: null,
+  username: "username1",
+  password: "password1",
+  usernameField: "",
+  passwordField: ""
+};
+
+const LoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1", "nsILoginInfo", "init");
+
+let BrowserApp;
+let browser;
+
+function add_login(login) {
+  let newLogin = new LoginInfo(login.hostname,
+                               login.formSubmitUrl,
+                               login.realmAny,
+                               login.username,
+                               login.password,
+                               login.usernameField,
+                               login.passwordField);
+
+  Services.logins.addLogin(newLogin);
+}
+
+add_test(function password_setup() {
+  add_login(LOGIN_FIELDS);
+
+  // Load about:passwords.
+  BrowserApp = Services.wm.getMostRecentWindow("navigator:browser").BrowserApp;
+  browser = BrowserApp.addTab("about:passwords", { selected: true, parentId: BrowserApp.selectedTab.id }).browser;
+
+  browser.addEventListener("load", () => {
+    browser.removeEventListener("load", this, true);
+    Services.tm.mainThread.dispatch(run_next_test, Ci.nsIThread.DISPATCH_NORMAL);
+  }, true);
+});
+
+add_test(function test_passwords_list() {
+  // Test that the (single) entry added in setup is correct.
+  let logins_list = browser.contentDocument.getElementById("logins-list");
+
+  let hostname = logins_list.querySelector(".hostname");
+  do_check_eq(hostname.textContent, LOGIN_FIELDS.hostname);
+
+  let username = logins_list.querySelector(".username");
+  do_check_eq(username.textContent, LOGIN_FIELDS.username);
+
+  let login_item = browser.contentDocument.querySelector("#logins-list > .login-item");
+  browser.addEventListener("PasswordsDetailsLoad", function() {
+    browser.removeEventListener("PasswordsDetailsLoad", this, false);
+    Services.tm.mainThread.dispatch(run_next_test, Ci.nsIThread.DISPATCH_NORMAL);
+  }, false);
+
+  // Expand item details.
+  login_item.click();
+});
+
+add_test(function test_passwords_details() {
+  let login_details = browser.contentDocument.getElementById("login-details");
+
+  let hostname = login_details.querySelector(".hostname");
+  do_check_eq(hostname.textContent, LOGIN_FIELDS.hostname);
+  let username = login_details.querySelector(".username");
+  do_check_eq(username.textContent, LOGIN_FIELDS.username);
+
+  // Check that details page opens link to host.
+  BrowserApp.deck.addEventListener("TabOpen", (tabevent) => {
+    // Wait for tab to finish loading.
+    let browser_target = tabevent.target;
+    browser_target.addEventListener("load", () => {
+      browser_target.removeEventListener("load", this, true);
+
+      do_check_eq(BrowserApp.selectedTab.browser.currentURI.spec, LOGIN_FIELDS.hostname);
+      Services.tm.mainThread.dispatch(run_next_test, Ci.nsIThread.DISPATCH_NORMAL);
+    }, true);
+
+    BrowserApp.deck.removeEventListener("TabOpen", this, false);
+  }, false);
+
+  browser.contentDocument.getElementById("details-header").click();
+});
+
+run_next_test();
--- a/mobile/android/base/tests/testDeviceSearchEngine.java
+++ b/mobile/android/base/tests/testDeviceSearchEngine.java
@@ -1,9 +1,11 @@
-package org.mozilla.gecko.tests;
+/* 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/. */
 
-
+package org.mozilla.gecko.tests;
 
 public class testDeviceSearchEngine extends JavascriptTest {
     public testDeviceSearchEngine() {
         super("testDeviceSearchEngine.js");
     }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/chrome/content/aboutPasswords.js
@@ -0,0 +1,224 @@
+/* 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 Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm")
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(window, "gChromeWin", function()
+  window.QueryInterface(Ci.nsIInterfaceRequestor)
+    .getInterface(Ci.nsIWebNavigation)
+    .QueryInterface(Ci.nsIDocShellTreeItem)
+    .rootTreeItem
+    .QueryInterface(Ci.nsIInterfaceRequestor)
+    .getInterface(Ci.nsIDOMWindow)
+    .QueryInterface(Ci.nsIDOMChromeWindow));
+
+let debug = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.d.bind(null, "AboutPasswords");
+
+let gStringBundle = Services.strings.createBundle("chrome://browser/locale/aboutPasswords.properties");
+
+function copyStringAndToast(string, notifyString) {
+  try {
+    let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
+    clipboard.copyString(string);
+    gChromeWin.NativeWindow.toast.show(notifyString, "short");
+  } catch (e) {
+    debug("Error copying from about:passwords");
+    gChromeWin.NativeWindow.toast.show(gStringBundle.GetStringFromName("passwordsDetails.copyFailed"), "short");
+  }
+}
+
+let Passwords = {
+  init: function () {
+    window.addEventListener("popstate", this , false);
+
+    Services.obs.addObserver(this, "passwordmgr-storage-changed", false);
+
+    this._loadList();
+
+    document.getElementById("copyusername-btn").addEventListener("click", this._copyUsername.bind(this), false);
+    document.getElementById("copypassword-btn").addEventListener("click", this._copyPassword.bind(this), false);
+    document.getElementById("details-header").addEventListener("click", this._openLink.bind(this), false);
+
+    this._showList();
+  },
+
+  uninit: function () {
+    Services.obs.removeObserver(this, "passwordmgr-storage-changed");
+    window.removeEventListener("popstate", this, false);
+  },
+
+  _loadList: function () {
+    let logins;
+    try {
+      logins = Services.logins.getAllLogins();
+    } catch(e) {
+      // Master password was not entered
+      debug("Master password permissions error: " + e);
+      return;
+    }
+
+    logins.forEach(login => login.QueryInterface(Ci.nsILoginMetaInfo));
+
+    logins.sort((a, b) => a.hostname.localeCompare(b.hostname));
+
+    // Clear all content before filling the logins
+    let list = document.getElementById("logins-list");
+    list.innerHTML = "";
+    logins.forEach(login => {
+      let item = this._createItemForLogin(login);
+      list.appendChild(item);
+    });
+  },
+
+  _showList: function () {
+    // Hide the detail page and show the list
+    let details = document.getElementById("login-details");
+    details.setAttribute("hidden", "true");
+    let list = document.getElementById("logins-list");
+    list.removeAttribute("hidden");
+  },
+
+  _onPopState: function (event) {
+    // Called when back/forward is used to change the state of the page
+    if (event.state) {
+      // Show the detail page for an addon
+      this._showDetails(this._getElementForLogin(event.state.id));
+    } else {
+      // Clear any previous detail addon
+      let detailItem = document.querySelector("#login-details > .login-item");
+      detailItem.login = null;
+      this._showList();
+    }
+  },
+
+  _createItemForLogin: function (login) {
+    let loginItem = document.createElement("div");
+
+    loginItem.setAttribute("loginID", login.guid);
+    loginItem.className = "login-item list-item";
+    loginItem.addEventListener("click", () => {
+      this._showDetails(loginItem);
+      history.pushState({ id: login.guid }, document.title);
+    }, true);
+
+    // Create item icon.
+    let img = document.createElement("img");
+    img.className = "icon";
+    img.setAttribute("src", login.hostname + "/favicon.ico");
+    loginItem.appendChild(img);
+
+    // Create item details.
+    let inner = document.createElement("div");
+    inner.className = "inner";
+
+    let details = document.createElement("div");
+    details.className = "details";
+    inner.appendChild(details);
+
+    let titlePart = document.createElement("div");
+    titlePart.className = "hostname";
+    titlePart.textContent = login.hostname;
+    details.appendChild(titlePart);
+
+    let versionPart = document.createElement("div");
+    versionPart.textContent = login.httpRealm;
+    versionPart.className = "realm";
+    details.appendChild(versionPart);
+
+    let descPart = document.createElement("div");
+    descPart.textContent = login.username;
+    descPart.className = "username";
+    inner.appendChild(descPart);
+
+    loginItem.appendChild(inner);
+    loginItem.login = login;
+    return loginItem;
+  },
+
+  _getElementForLogin: function (login) {
+    let list = document.getElementById("logins-list");
+    let element = list.querySelector("div[loginID=" + login.quote() + "]");
+    return element;
+  },
+
+  handleEvent: function (event) {
+    switch (event.type) {
+      case "popstate": {
+        this._onPopState(event);
+        break;
+      }
+    }
+  },
+
+  observe: function (subject, topic, data) {
+    switch(topic) {
+      case "passwordmgr-storage-changed": {
+        // Reload passwords content.
+        this._loadList();
+        break;
+      }
+    }
+  },
+
+  _showDetails: function (listItem) {
+    let detailItem = document.querySelector("#login-details > .login-item");
+    let login = detailItem.login = listItem.login;
+    let favicon = detailItem.querySelector(".icon");
+    favicon.setAttribute("src", login.hostname + "/favicon.ico");
+
+    document.getElementById("details-header").setAttribute("link", login.hostname);
+
+    document.getElementById("detail-hostname").textContent = login.hostname;
+    document.getElementById("detail-realm").textContent = login.httpRealm;
+    document.getElementById("detail-username").textContent = login.username;
+
+    // Borrowed from desktop Firefox: http://mxr.mozilla.org/mozilla-central/source/browser/base/content/urlbarBindings.xml#204
+    let matchedURL = login.hostname.match(/^((?:[a-z]+:\/\/)?(?:[^\/]+@)?)(.+?)(?::\d+)?(?:\/|$)/);
+
+    let userInputs = [];
+    if (matchedURL) {
+      let [, , domain] = matchedURL;
+      userInputs = domain.split(".").filter(part => part.length > 3);
+    }
+
+    let lastChanged = new Date(login.timePasswordChanged);
+    let days = Math.round((Date.now() - lastChanged) / 1000 / 60 / 60/ 24);
+    document.getElementById("detail-age").textContent = gStringBundle.formatStringFromName("passwordsDetails.age", [days], 1);
+
+    let list = document.getElementById("logins-list");
+    list.setAttribute("hidden", "true");
+
+    let loginDetails = document.getElementById("login-details");
+    loginDetails.removeAttribute("hidden");
+
+    // Password details page is loaded.
+    let loadEvent = document.createEvent("Events");
+    loadEvent.initEvent("PasswordsDetailsLoad", true, false);
+    window.dispatchEvent(loadEvent);
+  },
+
+  _copyUsername: function() {
+    let detailItem = document.querySelector("#login-details > .login-item");
+    let login = detailItem.login;
+    copyStringAndToast(login.username, gStringBundle.GetStringFromName("passwordsDetails.usernameCopied"));
+  },
+
+  _copyPassword: function() {
+    let detailItem = document.querySelector("#login-details > .login-item");
+    let login = detailItem.login;
+    copyStringAndToast(login.password, gStringBundle.GetStringFromName("passwordsDetails.passwordCopied"));
+  },
+
+  _openLink: function (clickEvent) {
+    let url = clickEvent.currentTarget.getAttribute("link");
+    let BrowserApp = gChromeWin.BrowserApp;
+    BrowserApp.addTab(url, { selected: true, parentId: BrowserApp.selectedTab.id });
+  }
+};
+
+window.addEventListener("load", Passwords.init.bind(Passwords), false);
+window.addEventListener("unload", Passwords.uninit.bind(Passwords), false);
new file mode 100644
--- /dev/null
+++ b/mobile/android/chrome/content/aboutPasswords.xhtml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [
+<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" >
+%globalDTD;
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % aboutDTD SYSTEM "chrome://browser/locale/aboutPasswords.dtd" >
+%aboutDTD;
+]>
+<!-- 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/. -->
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <title>&aboutPasswords.title;</title>
+    <meta name="viewport" content="width=device-width; user-scalable=0" />
+    <link rel="icon" type="image/png" sizes="64x64" href="chrome://branding/content/favicon64.png" />
+    <link rel="stylesheet" href="chrome://browser/skin/aboutBase.css" type="text/css"/>
+    <link rel="stylesheet" href="chrome://browser/skin/aboutPasswords.css" type="text/css"/>
+    <script type="application/javascript;version=1.8" src="chrome://browser/content/aboutPasswords.js"></script>
+  </head>
+  <body dir="&locale.dir;">
+    <div id="passwords-header" class="header">
+      <div>&aboutPasswords.title;</div>
+    </div>
+    <div id="logins-list" class="list" hidden="true">
+    </div>
+    <div id="login-details" class="list" hidden="true">
+      <div class="login-item list-item">
+        <img class="icon"/>
+        <div id="details-header" class="inner">
+          <div class="details">
+            <div id="detail-hostname" class="hostname"></div>
+            <div id="detail-realm" class="realm"></div>
+          </div>
+          <div id="detail-username" class="username"></div>
+          <div id="detail-age"></div>
+        </div>
+        <div class="buttons">
+          <button id="copyusername-btn">&aboutPasswords.copyUsername;</button>
+          <button id="copypassword-btn">&aboutPasswords.copyPassword;</button>
+        </div>
+      </div>
+    </div>
+  </body>
+</html>
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -7350,18 +7350,20 @@ var RemoteDebugger = {
         DebuggerServer.init();
         DebuggerServer.addBrowserActors();
         DebuggerServer.registerModule("resource://gre/modules/dbg-browser-actors.js");
       }
 
       let pathOrPort = this._getPath();
       if (!pathOrPort)
         pathOrPort = this._getPort();
-      let listener = DebuggerServer.openListener(pathOrPort);
+      let listener = DebuggerServer.createListener();
+      listener.portOrPath = pathOrPort;
       listener.allowConnection = this._showConnectionPrompt.bind(this);
+      listener.open();
       dump("Remote debugger listening at path " + pathOrPort);
     } catch(e) {
       dump("Remote debugger didn't start: " + e);
     }
   },
 
   _stop: function rd_start() {
     DebuggerServer.closeAllListeners();
--- a/mobile/android/chrome/jar.mn
+++ b/mobile/android/chrome/jar.mn
@@ -59,16 +59,18 @@ chrome.jar:
   content/aboutHealthReport.xhtml      (content/aboutHealthReport.xhtml)
 * content/aboutHealthReport.js         (content/aboutHealthReport.js)
 #endif
 #ifdef MOZ_DEVICES
   content/aboutDevices.xhtml           (content/aboutDevices.xhtml)
   content/aboutDevices.js              (content/aboutDevices.js)
 #endif
 #ifdef NIGHTLY_BUILD
+  content/aboutPasswords.xhtml         (content/aboutPasswords.xhtml)
+  content/aboutPasswords.js            (content/aboutPasswords.js)
   content/WebcompatReporter.js         (content/WebcompatReporter.js)
 #endif
 
 % content branding %content/branding/
 
 % override chrome://global/content/config.xul chrome://browser/content/config.xhtml
 % override chrome://global/content/netError.xhtml chrome://browser/content/netError.xhtml
 % override chrome://mozapps/content/extensions/extensions.xul chrome://browser/content/aboutAddons.xhtml
--- a/mobile/android/components/AboutRedirector.js
+++ b/mobile/android/components/AboutRedirector.js
@@ -79,16 +79,22 @@ let modules = {
   },
 #endif
 #ifdef MOZ_DEVICES
   devices: {
     uri: "chrome://browser/content/aboutDevices.xhtml",
     privileged: true
   },
 #endif
+#ifdef NIGHTLY_BUILD
+  passwords: {
+    uri: "chrome://browser/content/aboutPasswords.xhtml",
+    privileged: true
+  }
+#endif
 }
 
 function AboutRedirector() {}
 AboutRedirector.prototype = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]),
   classID: Components.ID("{322ba47e-7047-4f71-aebf-cb7d69325cd9}"),
 
   _getModuleInfo: function (aURI) {
@@ -108,17 +114,17 @@ AboutRedirector.prototype = {
 
   newChannel: function(aURI) {
     let moduleInfo = this._getModuleInfo(aURI);
 
     var ios = Cc["@mozilla.org/network/io-service;1"].
               getService(Ci.nsIIOService);
 
     var channel = ios.newChannel(moduleInfo.uri, null, null);
-    
+
     if (!moduleInfo.privileged) {
       // Setting the owner to null means that we'll go through the normal
       // path in GetChannelPrincipal and create a codebase principal based
       // on the channel's originalURI
       channel.owner = null;
     }
 
     channel.originalURI = aURI;
--- a/mobile/android/components/MobileComponents.manifest
+++ b/mobile/android/components/MobileComponents.manifest
@@ -16,16 +16,19 @@ contract @mozilla.org/network/protocol/a
 contract @mozilla.org/network/protocol/about;1?what=healthreport {322ba47e-7047-4f71-aebf-cb7d69325cd9}
 #endif
 #ifdef MOZ_SAFE_BROWSING
 contract @mozilla.org/network/protocol/about;1?what=blocked {322ba47e-7047-4f71-aebf-cb7d69325cd9}
 #endif
 #ifdef MOZ_DEVICES
 contract @mozilla.org/network/protocol/about;1?what=devices {322ba47e-7047-4f71-aebf-cb7d69325cd9}
 #endif
+#ifdef NIGHTLY_BUILD
+contract @mozilla.org/network/protocol/about;1?what=passwords {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+#endif
 
 # DirectoryProvider.js
 component {ef0f7a87-c1ee-45a8-8d67-26f586e46a4b} DirectoryProvider.js
 contract @mozilla.org/browser/directory-provider;1 {ef0f7a87-c1ee-45a8-8d67-26f586e46a4b}
 category xpcom-directory-providers browser-directory-provider @mozilla.org/browser/directory-provider;1
 
 # Sidebar.js
 component {22117140-9c6e-11d3-aaf1-00805f8a4905} Sidebar.js
new file mode 100644
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/aboutPasswords.dtd
@@ -0,0 +1,8 @@
+<!-- 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/. -->
+
+<!ENTITY aboutPasswords.title                    "Passwords">
+
+<!ENTITY aboutPasswords.copyUsername             "Copy Username">
+<!ENTITY aboutPasswords.copyPassword             "Copy Password">
new file mode 100644
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/aboutPasswords.properties
@@ -0,0 +1,9 @@
+# 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/.
+
+passwordsDetails.age=Age: %S days
+
+passwordsDetails.copyFailed=Copy failed
+passwordsDetails.passwordCopied=Password copied
+passwordsDetails.usernameCopied=Username copied
--- a/mobile/android/locales/jar.mn
+++ b/mobile/android/locales/jar.mn
@@ -35,16 +35,18 @@
   locale/@AB_CD@/browser/sync.properties          (%chrome/sync.properties)
   locale/@AB_CD@/browser/prompt.dtd               (%chrome/prompt.dtd)
   locale/@AB_CD@/browser/feedback.dtd             (%chrome/feedback.dtd)
   locale/@AB_CD@/browser/phishing.dtd             (%chrome/phishing.dtd)
   locale/@AB_CD@/browser/payments.properties      (%chrome/payments.properties)
   locale/@AB_CD@/browser/handling.properties      (%chrome/handling.properties)
   locale/@AB_CD@/browser/webapp.properties        (%chrome/webapp.properties)
 #ifdef NIGHTLY_BUILD
+  locale/@AB_CD@/browser/aboutPasswords.dtd       (%chrome/aboutPasswords.dtd)
+  locale/@AB_CD@/browser/aboutPasswords.properties (%chrome/aboutPasswords.properties)
   locale/@AB_CD@/browser/webcompatReporter.properties (%chrome/webcompatReporter.properties)
 #endif
 
 # overrides for toolkit l10n, also for en-US
 relativesrcdir toolkit/locales:
   locale/@AB_CD@/browser/overrides/about.dtd                       (%chrome/global/about.dtd)
   locale/@AB_CD@/browser/overrides/aboutAbout.dtd                  (%chrome/global/aboutAbout.dtd)
   locale/@AB_CD@/browser/overrides/aboutRights.dtd                 (%chrome/global/aboutRights.dtd)
new file mode 100644
--- /dev/null
+++ b/mobile/android/themes/core/aboutPasswords.css
@@ -0,0 +1,32 @@
+/* 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/. */
+.hidden {
+  display: none;
+}
+
+.details {
+  width: 100%;
+}
+
+.details > div {
+  display: inline;
+}
+
+.username {
+  width: 100%;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+
+.hostname {
+  font-weight: bold;
+  overflow: hidden;
+  flex: 1;
+}
+
+.realm {
+  /* hostname is not localized, so keep the margin on the left side */
+  margin-left: .67em;
+}
--- a/mobile/android/themes/core/jar.mn
+++ b/mobile/android/themes/core/jar.mn
@@ -29,16 +29,20 @@ chrome.jar:
   skin/touchcontrols.css                    (touchcontrols.css)
   skin/netError.css                         (netError.css)
 % override chrome://global/skin/about.css chrome://browser/skin/about.css
 % override chrome://global/skin/aboutMemory.css chrome://browser/skin/aboutMemory.css
 % override chrome://global/skin/aboutSupport.css chrome://browser/skin/aboutSupport.css
 % override chrome://global/skin/media/videocontrols.css chrome://browser/skin/touchcontrols.css
 % override chrome://global/skin/netError.css chrome://browser/skin/netError.css
 
+#ifdef NIGHTLY_BUILD
+  skin/aboutPasswords.css                   (aboutPasswords.css)
+#endif
+
   skin/images/search.png                    (images/search.png)
   skin/images/lock.png                      (images/lock.png)
   skin/images/textfield.png                 (images/textfield.png)
 
   skin/images/5stars.png                    (images/5stars.png)
   skin/images/addons-32.png                 (images/addons-32.png)
   skin/images/amo-logo.png                  (images/amo-logo.png)
   skin/images/arrowleft-16.png              (images/arrowleft-16.png)
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -794,16 +794,18 @@ pref("devtools.dump.emit", false);
 // Disable device discovery logging
 pref("devtools.discovery.log", false);
 // Disable scanning for DevTools devices via WiFi
 pref("devtools.remote.wifi.scan", false);
 // Hide UI options for controlling device visibility over WiFi
 // N.B.: This does not set whether the device can be discovered via WiFi, only
 // whether the UI control to make such a choice is shown to the user
 pref("devtools.remote.wifi.visible", false);
+// Client must complete TLS handshake within this window (ms)
+pref("devtools.remote.tls-handshake-timeout", 10000);
 
 // view source
 pref("view_source.syntax_highlight", true);
 pref("view_source.wrap_long_lines", false);
 pref("view_source.editor.external", false);
 pref("view_source.editor.path", "");
 // allows to add further arguments to the editor; use the %LINE% placeholder
 // for jumping to a specific line (e.g. "/line:%LINE%" or "--goto %LINE%")
--- a/testing/xpcshell/head.js
+++ b/testing/xpcshell/head.js
@@ -331,17 +331,19 @@ function _register_modules_protocol_hand
                     _TESTING_MODULES_DIR);
   }
 
   let modulesURI = ios.newFileURI(modulesFile);
 
   protocolHandler.setSubstitution("testing-common", modulesURI);
 }
 
-function _initDebugging(port) {
+/* Debugging support */
+// Used locally and by our self-tests.
+function _setupDebuggerServer(breakpointFiles, callback) {
   let prefs = Components.classes["@mozilla.org/preferences-service;1"]
               .getService(Components.interfaces.nsIPrefBranch);
 
   // Always allow remote debugging.
   prefs.setBoolPref("devtools.debugger.remote-enabled", true);
 
   // for debugging-the-debugging, let an env var cause log spew.
   let env = Components.classes["@mozilla.org/process/environment;1"]
@@ -357,62 +359,69 @@ function _initDebugging(port) {
   DebuggerServer.init();
   DebuggerServer.addBrowserActors();
   DebuggerServer.addActors("resource://testing-common/dbg-actors.js");
 
   // An observer notification that tells us when we can "resume" script
   // execution.
   let obsSvc = Components.classes["@mozilla.org/observer-service;1"].
                getService(Components.interfaces.nsIObserverService);
-  let initialized = false;
 
   const TOPICS = ["devtools-thread-resumed", "xpcshell-test-devtools-shutdown"];
   let observe = function(subject, topic, data) {
     switch (topic) {
       case "devtools-thread-resumed":
         // Exceptions in here aren't reported and block the debugger from
         // resuming, so...
         try {
           // Add a breakpoint for the first line in our test files.
           let threadActor = subject.wrappedJSObject;
           let location = { line: 1 };
-          for (let file of _TEST_FILE) {
+          for (let file of breakpointFiles) {
             let sourceActor = threadActor.sources.source({originalUrl: file});
-            sourceActor.createAndStoreBreakpoint(location);
+            sourceActor.setBreakpoint(location);
           }
         } catch (ex) {
           do_print("Failed to initialize breakpoints: " + ex + "\n" + ex.stack);
         }
         break;
       case "xpcshell-test-devtools-shutdown":
         // the debugger has shutdown before we got a resume event - nothing
         // special to do here.
         break;
     }
-    initialized = true;
     for (let topicToRemove of TOPICS) {
       obsSvc.removeObserver(observe, topicToRemove);
     }
+    callback();
   };
 
   for (let topic of TOPICS) {
     obsSvc.addObserver(observe, topic, false);
   }
+  return DebuggerServer;
+}
+
+function _initDebugging(port) {
+  let initialized = false;
+  let DebuggerServer = _setupDebuggerServer(_TEST_FILE, () => {initialized = true;});
 
   do_print("");
   do_print("*******************************************************************");
   do_print("Waiting for the debugger to connect on port " + port)
   do_print("")
   do_print("To connect the debugger, open a Firefox instance, select 'Connect'");
   do_print("from the Developer menu and specify the port as " + port);
   do_print("*******************************************************************");
   do_print("")
 
-  let listener = DebuggerServer.openListener(port);
+  let listener = DebuggerServer.createListener();
+  listener.portOrPath = port;
   listener.allowConnection = () => true;
+  listener.open();
 
   // spin an event loop until the debugger connects.
   let thr = Components.classes["@mozilla.org/thread-manager;1"]
               .getService().currentThread;
   while (!initialized) {
     do_print("Still waiting for debugger to connect...");
     thr.processNextEvent(true);
   }
--- a/toolkit/components/addoncompat/RemoteAddonsChild.jsm
+++ b/toolkit/components/addoncompat/RemoteAddonsChild.jsm
@@ -199,17 +199,17 @@ AboutProtocolChannel.prototype = {
     // Ask the parent to synchronously read all the data from the channel.
     let cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
                .getService(Ci.nsISyncMessageSender);
     let rval = cpmm.sendRpcMessage("Addons:AboutProtocol:OpenChannel", {
       uri: this.URI.spec,
       contractID: this._contractID
     }, {
       notificationCallbacks: this.notificationCallbacks,
-      loadGroupNotificationCallbacks: this.loadGroup.notificationCallbacks
+      loadGroupNotificationCallbacks: this.loadGroup ? this.loadGroup.notificationCallbacks : null,
     });
 
     if (rval.length != 1) {
       throw Cr.NS_ERROR_FAILURE;
     }
 
     let {data, contentType} = rval[0];
     this.contentType = contentType;
--- a/toolkit/components/addoncompat/RemoteAddonsParent.jsm
+++ b/toolkit/components/addoncompat/RemoteAddonsParent.jsm
@@ -236,17 +236,21 @@ let AboutProtocolParent = {
   // return it to the child.
   openChannel: function(msg) {
     let uri = BrowserUtils.makeURI(msg.data.uri);
     let contractID = msg.data.contractID;
     let module = Cc[contractID].getService(Ci.nsIAboutModule);
     try {
       let channel = module.newChannel(uri, null);
       channel.notificationCallbacks = msg.objects.notificationCallbacks;
-      channel.loadGroup = {notificationCallbacks: msg.objects.loadGroupNotificationCallbacks};
+      if (msg.objects.loadGroupNotificationCallbacks) {
+        channel.loadGroup = {notificationCallbacks: msg.objects.loadGroupNotificationCallbacks};
+      } else {
+        channel.loadGroup = null;
+      }
       let stream = channel.open();
       let data = NetUtil.readInputStreamToString(stream, stream.available(), {});
       return {
         data: data,
         contentType: channel.contentType
       };
     } catch (e) {
       Cu.reportError(e);
--- a/toolkit/components/addoncompat/tests/addon/bootstrap.js
+++ b/toolkit/components/addoncompat/tests/addon/bootstrap.js
@@ -1,14 +1,15 @@
 var Cc = Components.classes;
 var Ci = Components.interfaces;
 var Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/BrowserUtils.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 const baseURL = "http://mochi.test:8888/browser/" +
   "toolkit/components/addoncompat/tests/browser/";
 
 function forEachWindow(f)
 {
   let wins = Services.ww.getWindowEnumerator("navigator:browser");
   while (wins.hasMoreElements()) {
@@ -253,31 +254,191 @@ function testAddonContent()
       gBrowser.removeTab(tab);
       res.setSubstitution("addonshim1", null);
 
       resolve();
     });
   });
 }
 
+
+// Test for bug 1102410. We check that multiple nsIAboutModule's can be
+// registered in the parent, and that the child can browse to each of
+// the registered about: pages.
+function testAboutModuleRegistration()
+{
+  let Registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+
+  let modulesToUnregister = new Map();
+
+  /**
+   * This function creates a new nsIAboutModule and registers it. Callers
+   * should also call unregisterModules after using this function to clean
+   * up the nsIAboutModules at the end of this test.
+   *
+   * @param aboutName
+   *        This will be the string after about: used to refer to this module.
+   *        For example, if aboutName is foo, you can refer to this module by
+   *        browsing to about:foo.
+   *
+   * @param uuid
+   *        A unique identifer string for this module. For example,
+   *        "5f3a921b-250f-4ac5-a61c-8f79372e6063"
+   */
+  let createAndRegisterAboutModule = function(aboutName, uuid) {
+
+    let AboutModule = function() {};
+
+    AboutModule.prototype = {
+      classID: Components.ID(uuid),
+      classDescription: `Testing About Module for about:${aboutName}`,
+      contractID: `@mozilla.org/network/protocol/about;1?what=${aboutName}`,
+      QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]),
+
+      newChannel: (aURI) => {
+        let uri = Services.io.newURI(`data:,<html><h1>${aboutName}</h1></html>`, null, null);
+        let chan = Services.io.newChannelFromURI(uri);
+        chan.originalURI = aURI;
+        return chan;
+      },
+
+      getURIFlags: (aURI) => {
+        return Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT |
+               Ci.nsIAboutModule.ALLOW_SCRIPT;
+      },
+    };
+
+    let factory = {
+      createInstance: function(outer, iid) {
+        if (outer) {
+          throw Cr.NS_ERROR_NO_AGGREGATION;
+        }
+        return new AboutModule();
+      },
+    };
+
+    Registrar.registerFactory(AboutModule.prototype.classID,
+                              AboutModule.prototype.classDescription,
+                              AboutModule.prototype.contractID,
+                              factory);
+
+    modulesToUnregister.set(AboutModule.prototype.classID,
+                            factory);
+  };
+
+  /**
+   * Unregisters any nsIAboutModules registered with
+   * createAndRegisterAboutModule.
+   */
+  let unregisterModules = () => {
+    for (let [classID, factory] of modulesToUnregister) {
+      Registrar.unregisterFactory(classID, factory);
+    }
+  };
+
+  /**
+   * Takes a browser, and sends it a framescript to attempt to
+   * load some about: pages. The frame script will send a test:result
+   * message on completion, passing back a data object with:
+   *
+   * {
+   *   pass: true
+   * }
+   *
+   * on success, and:
+   *
+   * {
+   *   pass: false,
+   *   errorMsg: message,
+   * }
+   *
+   * on failure.
+   *
+   * @param browser
+   *        The browser to send the framescript to.
+   */
+  let testAboutModulesWork = (browser) => {
+    let testConnection = () => {
+      const XMLHttpRequest = Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1",
+                                                    "nsIXMLHttpRequest");
+      let request = new XMLHttpRequest();
+      try {
+        request.open("GET", "about:test1", false);
+        request.send(null);
+        if (request.status != 200) {
+          throw(`about:test1 response had status ${request.status} - expected 200`);
+        }
+
+        request = new XMLHttpRequest();
+        request.open("GET", "about:test2", false);
+        request.send(null);
+
+        if (request.status != 200) {
+          throw(`about:test2 response had status ${request.status} - expected 200`);
+        }
+
+        sendAsyncMessage("test:result", {
+          pass: true,
+        });
+      } catch(e) {
+        sendAsyncMessage("test:result", {
+          pass: false,
+          errorMsg: e.toString(),
+        });
+      }
+    };
+
+    return new Promise((resolve, reject) => {
+      let mm = browser.messageManager;
+      mm.addMessageListener("test:result", function onTestResult(message) {
+        mm.removeMessageListener("test:result", onTestResult);
+        if (message.data.pass) {
+          ok(true, "Connections to about: pages were successful");
+        } else {
+          ok(false, message.data.errorMsg);
+        }
+        resolve();
+      });
+      mm.loadFrameScript("data:,(" + testConnection.toString() + ")();", false);
+    });
+  }
+
+  // Here's where the actual test is performed.
+  return new Promise((resolve, reject) => {
+    createAndRegisterAboutModule("test1", "5f3a921b-250f-4ac5-a61c-8f79372e6063");
+    createAndRegisterAboutModule("test2", "d7ec0389-1d49-40fa-b55c-a1fc3a6dbf6f");
+
+    let newTab = gBrowser.addTab();
+    gBrowser.selectedTab = newTab;
+    let browser = newTab.linkedBrowser;
+
+    testAboutModulesWork(browser).then(() => {
+      gBrowser.removeTab(newTab);
+      unregisterModules();
+      resolve();
+    });
+  });
+}
+
 function runTests(win, funcs)
 {
   ok = funcs.ok;
   is = funcs.is;
   info = funcs.info;
 
   gWin = win;
   gBrowser = win.gBrowser;
 
   return testContentWindow().
     then(testListeners).
     then(testCapturing).
     then(testObserver).
     then(testSandbox).
-    then(testAddonContent);
+    then(testAddonContent).
+    then(testAboutModuleRegistration);
 }
 
 /*
  bootstrap.js API
 */
 
 function startup(aData, aReason)
 {
--- a/toolkit/components/filewatcher/NativeFileWatcherWin.cpp
+++ b/toolkit/components/filewatcher/NativeFileWatcherWin.cpp
@@ -560,24 +560,24 @@ NativeFileWatcherIOTask::Run()
  *         otherwise NS_OK.
  */
 nsresult
 NativeFileWatcherIOTask::AddPathRunnableMethod(
   PathRunnablesParametersWrapper* aWrappedParameters)
 {
   MOZ_ASSERT(!NS_IsMainThread());
 
+  nsAutoPtr<PathRunnablesParametersWrapper> wrappedParameters(aWrappedParameters);
+
   // We return immediately if |mShuttingDown| is true (see below for
   // details about the shutdown protocol being followed).
   if (mShuttingDown) {
     return NS_OK;
   }
 
-  nsAutoPtr<PathRunnablesParametersWrapper> wrappedParameters(aWrappedParameters);
-
   if (!wrappedParameters ||
       !wrappedParameters->mChangeCallbackHandle) {
     FILEWATCHERLOG("NativeFileWatcherIOTask::AddPathRunnableMethod - Invalid arguments.");
     return NS_ERROR_NULL_POINTER;
   }
 
   // Is aPathToWatch already being watched?
   WatchedResourceDescriptor* watchedResource =
@@ -730,24 +730,24 @@ NativeFileWatcherIOTask::AddPathRunnable
  *        handles.
  */
 nsresult
 NativeFileWatcherIOTask::RemovePathRunnableMethod(
   PathRunnablesParametersWrapper* aWrappedParameters)
 {
   MOZ_ASSERT(!NS_IsMainThread());
 
+  nsAutoPtr<PathRunnablesParametersWrapper> wrappedParameters(aWrappedParameters);
+
   // We return immediately if |mShuttingDown| is true (see below for
   // details about the shutdown protocol being followed).
   if (mShuttingDown) {
     return NS_OK;
   }
 
-  nsAutoPtr<PathRunnablesParametersWrapper> wrappedParameters(aWrappedParameters);
-
   if (!wrappedParameters ||
       !wrappedParameters->mChangeCallbackHandle) {
     return NS_ERROR_NULL_POINTER;
   }
 
   WatchedResourceDescriptor* toRemove =
     mWatchedResourcesByPath.Get(wrappedParameters->mPath);
   if (!toRemove) {
--- a/toolkit/components/jsdownloads/moz.build
+++ b/toolkit/components/jsdownloads/moz.build
@@ -2,8 +2,9 @@
 # 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/.
 
 DIRS += ['public', 'src']
 
 XPCSHELL_TESTS_MANIFESTS += ['test/data/xpcshell.ini', 'test/unit/xpcshell.ini']
+BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
--- a/toolkit/components/jsdownloads/src/DownloadCore.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm
@@ -25,28 +25,34 @@
  * DownloadSaver
  * Template for an object that actually transfers the data for the download.
  *
  * DownloadCopySaver
  * Saver object that simply copies the entire source file to the target.
  *
  * DownloadLegacySaver
  * Saver object that integrates with the legacy nsITransfer interface.
+ *
+ * DownloadPDFSaver
+ * This DownloadSaver type creates a PDF file from the current document in a
+ * given window, specified using the windowRef property of the DownloadSource
+ * object associated with the download.
  */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = [
   "Download",
   "DownloadSource",
   "DownloadTarget",
   "DownloadError",
   "DownloadSaver",
   "DownloadCopySaver",
   "DownloadLegacySaver",
+  "DownloadPDFSaver",
 ];
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Globals
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
@@ -63,26 +69,31 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm")
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+                                  "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "gDownloadHistory",
            "@mozilla.org/browser/download-history;1",
            Ci.nsIDownloadHistory);
 XPCOMUtils.defineLazyServiceGetter(this, "gExternalAppLauncher",
            "@mozilla.org/uriloader/external-helper-app-service;1",
            Ci.nsPIExternalAppLauncher);
 XPCOMUtils.defineLazyServiceGetter(this, "gExternalHelperAppService",
            "@mozilla.org/uriloader/external-helper-app-service;1",
            Ci.nsIExternalHelperAppService);
+XPCOMUtils.defineLazyServiceGetter(this, "gPrintSettingsService",
+           "@mozilla.org/gfx/printsettings-service;1",
+           Ci.nsIPrintSettingsService);
 
 const BackgroundFileSaverStreamListener = Components.Constructor(
       "@mozilla.org/network/background-file-saver;1?mode=streamlistener",
       "nsIBackgroundFileSaver");
 
 /**
  * Returns true if the given value is a primitive string or a String object.
  */
@@ -539,17 +550,17 @@ this.Download.prototype = {
    * @return {Promise}
    * @resolves When the instruction to launch the file has been
    *           successfully given to the operating system. Note that
    *           the OS might still take a while until the file is actually
    *           launched.
    * @rejects  JavaScript exception if there was an error trying to launch
    *           the file.
    */
-  launch: function() {
+  launch: function () {
     if (!this.succeeded) {
       return Promise.reject(
         new Error("launch can only be called if the download succeeded")
       );
     }
 
     return DownloadIntegration.launchDownload(this);
   },
@@ -906,21 +917,26 @@ this.Download.prototype = {
    */
   toSerializable: function ()
   {
     let serializable = {
       source: this.source.toSerializable(),
       target: this.target.toSerializable(),
     };
 
+    let saver = this.saver.toSerializable();
+    if (!saver) {
+      // If we are unable to serialize the saver, we won't persist the download.
+      return null;
+    }
+
     // Simplify the representation for the most common saver type.  If the saver
     // is an object instead of a simple string, we can't simplify it because we
     // need to persist all its properties, not only "type".  This may happen for
     // savers of type "copy" as well as other types.
-    let saver = this.saver.toSerializable();
     if (saver !== "copy") {
       serializable.saver = saver;
     }
 
     if (this.error) {
       serializable.errorObj = this.error.toSerializable();
     }
 
@@ -1121,16 +1137,20 @@ this.DownloadSource.prototype = {
  */
 this.DownloadSource.fromSerializable = function (aSerializable) {
   let source = new DownloadSource();
   if (isString(aSerializable)) {
     // Convert String objects to primitive strings at this point.
     source.url = aSerializable.toString();
   } else if (aSerializable instanceof Ci.nsIURI) {
     source.url = aSerializable.spec;
+  } else if (aSerializable instanceof Ci.nsIDOMWindow) {
+    source.url = aSerializable.location.href;
+    source.isPrivate = PrivateBrowsingUtils.isContentWindowPrivate(aSerializable);
+    source.windowRef = Cu.getWeakReference(aSerializable);
   } else {
     // Convert String objects to primitive strings at this point.
     source.url = aSerializable.url.toString();
     if ("isPrivate" in aSerializable) {
       source.isPrivate = aSerializable.isPrivate;
     }
     if ("referrer" in aSerializable) {
       source.referrer = aSerializable.referrer;
@@ -1521,16 +1541,19 @@ this.DownloadSaver.fromSerializable = fu
   let saver;
   switch (serializable.type) {
     case "copy":
       saver = DownloadCopySaver.fromSerializable(serializable);
       break;
     case "legacy":
       saver = DownloadLegacySaver.fromSerializable(serializable);
       break;
+    case "pdf":
+      saver = DownloadPDFSaver.fromSerializable(serializable);
+      break;
     default:
       throw new Error("Unrecoginzed download saver type.");
   }
   return saver;
 };
 
 ////////////////////////////////////////////////////////////////////////////////
 //// DownloadCopySaver
@@ -1944,17 +1967,17 @@ this.DownloadCopySaver.fromSerializable 
 ////////////////////////////////////////////////////////////////////////////////
 //// DownloadLegacySaver
 
 /**
  * Saver object that integrates with the legacy nsITransfer interface.
  *
  * For more background on the process, see the DownloadLegacyTransfer object.
  */
-this.DownloadLegacySaver = function()
+this.DownloadLegacySaver = function ()
 {
   this.deferExecuted = Promise.defer();
   this.deferCanceled = Promise.defer();
 }
 
 this.DownloadLegacySaver.prototype = {
   __proto__: DownloadSaver.prototype,
 
@@ -2297,8 +2320,164 @@ this.DownloadLegacySaver.prototype = {
 /**
  * Returns a new DownloadLegacySaver object.  This saver type has a
  * deserializable form only when creating a new object in memory, because it
  * cannot be serialized to disk.
  */
 this.DownloadLegacySaver.fromSerializable = function () {
   return new DownloadLegacySaver();
 };
+
+////////////////////////////////////////////////////////////////////////////////
+//// DownloadPDFSaver
+
+/**
+ * This DownloadSaver type creates a PDF file from the current document in a
+ * given window, specified using the windowRef property of the DownloadSource
+ * object associated with the download.
+ *
+ * In order to prevent the download from saving a different document than the one
+ * originally loaded in the window, any attempt to restart the download will fail.
+ *
+ * Since this DownloadSaver type requires a live document as a source, it cannot
+ * be persisted across sessions, unless the download already succeeded.
+ */
+this.DownloadPDFSaver = function () {
+}
+
+this.DownloadPDFSaver.prototype = {
+  __proto__: DownloadSaver.prototype,
+
+  /**
+   * An nsIWebBrowserPrint instance for printing this page.
+   * This is null when saving has not started or has completed,
+   * or while the operation is being canceled.
+   */
+  _webBrowserPrint: null,
+
+  /**
+   * Implements "DownloadSaver.execute".
+   */
+  execute: function (aSetProgressBytesFn, aSetPropertiesFn)
+  {
+    return Task.spawn(function task_DCS_execute() {
+      if (!this.download.source.windowRef) {
+        throw new DownloadError({
+          message: "PDF saver must be passed an open window, and cannot be restarted.",
+          becauseSourceFailed: true,
+        });
+      }
+
+      let win = this.download.source.windowRef.get();
+
+      // Set windowRef to null to avoid re-trying.
+      this.download.source.windowRef = null;
+
+      if (!win) {
+        throw new DownloadError({
+          message: "PDF saver can't save a window that has been closed.",
+          becauseSourceFailed: true,
+        });
+      }
+
+      this.addToHistory();
+
+      let targetPath = this.download.target.path;
+
+      // An empty target file must exist for the PDF printer to work correctly.
+      let file = yield OS.File.open(targetPath, { truncate: true });
+      yield file.close();
+
+      let printSettings = gPrintSettingsService.newPrintSettings;
+
+      printSettings.printToFile = true;
+      printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF;
+      printSettings.toFileName = targetPath;
+
+      printSettings.printSilent = true;
+      printSettings.showPrintProgress = false;
+
+      printSettings.printBGImages = true;
+      printSettings.printBGColors = true;
+      printSettings.printFrameType = Ci.nsIPrintSettings.kFramesAsIs;
+      printSettings.headerStrCenter = "";
+      printSettings.headerStrLeft = "";
+      printSettings.headerStrRight = "";
+      printSettings.footerStrCenter = "";
+      printSettings.footerStrLeft = "";
+      printSettings.footerStrRight = "";
+
+      this._webBrowserPrint = win.QueryInterface(Ci.nsIInterfaceRequestor)
+                                 .getInterface(Ci.nsIWebBrowserPrint);
+
+      try {
+        yield new Promise((resolve, reject) => {
+          this._webBrowserPrint.print(printSettings, {
+            onStateChange: function (webProgress, request, stateFlags, status) {
+              if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+                if (!Components.isSuccessCode(status)) {
+                  reject(new DownloadError({ result: status,
+                                             inferCause: true }));
+                } else {
+                  resolve();
+                }
+              }
+            },
+            onProgressChange: function (webProgress, request, curSelfProgress,
+                                        maxSelfProgress, curTotalProgress,
+                                        maxTotalProgress) {
+              aSetProgressBytesFn(curTotalProgress, maxTotalProgress, false);
+            },
+            onLocationChange: function () {},
+            onStatusChange: function () {},
+            onSecurityChange: function () {},
+          });
+        });
+      } finally {
+        // Remove the print object to avoid leaks
+        this._webBrowserPrint = null;
+      }
+
+      let fileInfo = yield OS.File.stat(targetPath);
+      aSetProgressBytesFn(fileInfo.size, fileInfo.size, false);
+    }.bind(this));
+  },
+
+  /**
+   * Implements "DownloadSaver.cancel".
+   */
+  cancel: function DCS_cancel()
+  {
+    if (this._webBrowserPrint) {
+      this._webBrowserPrint.cancel();
+      this._webBrowserPrint = null;
+    }
+  },
+
+  /**
+   * Implements "DownloadSaver.toSerializable".
+   */
+  toSerializable: function ()
+  {
+    if (this.download.succeeded) {
+      return DownloadCopySaver.prototype.toSerializable.call(this);
+    }
+
+    // This object needs a window to recreate itself. If it didn't succeded
+    // it will not be possible to restart. Returning null here will
+    // prevent us from serializing it at all.
+    return null;
+  },
+};
+
+/**
+ * Creates a new DownloadPDFSaver object, with its initial state derived from
+ * its serializable representation.
+ *
+ * @param aSerializable
+ *        Serializable representation of a DownloadPDFSaver object.
+ *
+ * @return The newly created DownloadPDFSaver object.
+ */
+this.DownloadPDFSaver.fromSerializable = function (aSerializable) {
+  return new DownloadPDFSaver();
+};
+
--- a/toolkit/components/jsdownloads/src/DownloadStore.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadStore.jsm
@@ -159,17 +159,23 @@ this.DownloadStore.prototype = {
       // Take a static snapshot of the current state of all the downloads.
       let storeData = { list: [] };
       let atLeastOneDownload = false;
       for (let download of downloads) {
         try {
           if (!this.onsaveitem(download)) {
             continue;
           }
-          storeData.list.push(download.toSerializable());
+
+          let serializable = download.toSerializable();
+          if (!serializable) {
+            // This item cannot be persisted across sessions.
+            continue;
+          }
+          storeData.list.push(serializable);
           atLeastOneDownload = true;
         } catch (ex) {
           // If an item cannot be converted to a serializable form, don't
           // prevent others from being saved.
           Cu.reportError(ex);
         }
       }
 
new file mode 100644
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/browser/browser.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+support-files =
+  head.js
+  testFile.html
+
+[browser_DownloadPDFSaver.js]
+skip-if = e10s || os != "win"
new file mode 100644
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/browser/browser_DownloadPDFSaver.js
@@ -0,0 +1,101 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the PDF download saver, and tests using a window as a
+ * source for the copy download saver.
+ */
+
+"use strict";
+
+/**
+ * Helper function to make sure a window reference exists on the download source.
+ */
+function* test_download_windowRef(aTab, aDownload) {
+  ok(aDownload.source.windowRef, "Download source had a window reference");
+  ok(aDownload.source.windowRef instanceof Ci.xpcIJSWeakReference, "Download window reference is a weak ref");
+  is(aDownload.source.windowRef.get(), aTab.linkedBrowser.contentWindow, "Download window exists during test");
+}
+
+/**
+ * Helper function to check the state of a completed download.
+ */
+function* test_download_state_complete(aTab, aDownload, aPrivate, aCanceled) {
+  ok(aDownload.source, "Download has a source");
+  is(aDownload.source.url, aTab.linkedBrowser.contentWindow.location, "Download source has correct url");
+  is(aDownload.source.isPrivate, aPrivate, "Download source has correct private state");
+  ok(aDownload.stopped, "Download is stopped");
+  is(aCanceled, aDownload.canceled, "Download has correct canceled state");
+  is(!aCanceled, aDownload.succeeded, "Download has correct succeeded state");
+  is(aDownload.error, null, "Download error is not defined");
+}
+
+function* test_createDownload_common(aPrivate, aType) {
+  let tab = gBrowser.addTab(getRootDirectory(gTestPath) + "testFile.html");
+  yield promiseBrowserLoaded(tab.linkedBrowser);
+
+  if (aPrivate) {
+    tab.linkedBrowser.docShell.QueryInterface(Ci.nsILoadContext)
+                              .usePrivateBrowsing = true;
+  }
+
+  let download = yield Downloads.createDownload({
+    source: tab.linkedBrowser.contentWindow,
+    target: { path: getTempFile(TEST_TARGET_FILE_NAME_PDF).path },
+    saver: { type: aType },
+  });
+
+  yield test_download_windowRef(tab, download);
+  yield download.start();
+
+  yield test_download_state_complete(tab, download, aPrivate, false);
+  if (aType == "pdf") {
+    let signature = yield OS.File.read(download.target.path,
+                                       { bytes: 4, encoding: "us-ascii" });
+    is(signature, "%PDF", "File exists and signature matches");
+  } else {
+    ok((yield OS.File.exists(download.target.path)), "File exists");
+  }
+
+  gBrowser.removeTab(tab);
+}
+
+add_task(function* test_createDownload_pdf_private() {
+  yield test_createDownload_common(true, "pdf");
+});
+add_task(function* test_createDownload_pdf_not_private() {
+  yield test_createDownload_common(false, "pdf");
+});
+
+// Even for the copy saver, using a window should produce valid results
+add_task(function* test_createDownload_copy_private() {
+  yield test_createDownload_common(true, "copy");
+});
+add_task(function* test_createDownload_copy_not_private() {
+  yield test_createDownload_common(false, "copy");
+});
+
+add_task(function* test_cancel_pdf_download() {
+  let tab = gBrowser.addTab(getRootDirectory(gTestPath) + "testFile.html");
+  yield promiseBrowserLoaded(tab.linkedBrowser);
+
+  let download = yield Downloads.createDownload({
+    source: tab.linkedBrowser.contentWindow,
+    target: { path: getTempFile(TEST_TARGET_FILE_NAME_PDF).path },
+    saver: "pdf",
+  });
+
+  yield test_download_windowRef(tab, download);
+  download.start();
+
+  // Immediately cancel the download to test that it is erased correctly.
+  yield download.cancel();
+  yield test_download_state_complete(tab, download, false, true);
+
+  let exists = yield OS.File.exists(download.target.path)
+  ok(!exists, "Target file does not exist");
+
+  gBrowser.removeTab(tab);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/browser/head.js
@@ -0,0 +1,89 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Provides infrastructure for automated download components tests.
+ */
+
+"use strict";
+
+////////////////////////////////////////////////////////////////////////////////
+//// Globals
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
+                                  "resource://gre/modules/DownloadPaths.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+                                  "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+                                  "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+                                  "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
+                                  "resource://testing-common/httpd.js");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+                                  "resource://gre/modules/osfile.jsm");
+
+const TEST_TARGET_FILE_NAME_PDF = "test-download.pdf";
+
+////////////////////////////////////////////////////////////////////////////////
+//// Support functions
+
+// While the previous test file should have deleted all the temporary files it
+// used, on Windows these might still be pending deletion on the physical file
+// system.  Thus, start from a new base number every time, to make a collision
+// with a file that is still pending deletion highly unlikely.
+let gFileCounter = Math.floor(Math.random() * 1000000);
+
+/**
+ * Returns a reference to a temporary file, that is guaranteed not to exist, and
+ * to have never been created before.
+ *
+ * @param aLeafName
+ *        Suggested leaf name for the file to be created.
+ *
+ * @return nsIFile pointing to a non-existent file in a temporary directory.
+ *
+ * @note It is not enough to delete the file if it exists, or to delete the file
+ *       after calling nsIFile.createUnique, because on Windows the delete
+ *       operation in the file system may still be pending, preventing a new
+ *       file with the same name to be created.
+ */
+function getTempFile(aLeafName)
+{
+  // Prepend a serial number to the extension in the suggested leaf name.
+  let [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName);
+  let leafName = base + "-" + gFileCounter + ext;
+  gFileCounter++;
+
+  // Get a file reference under the temporary directory for this test file.
+  let file = FileUtils.getFile("TmpD", [leafName]);
+  ok(!file.exists(), "Temp file does not exist");
+
+  registerCleanupFunction(function () {
+    if (file.exists()) {
+      file.remove(false);
+    }
+  });
+
+  return file;
+}
+
+function promiseBrowserLoaded(browser) {
+  return new Promise(resolve => {
+    browser.addEventListener("load", function onLoad(event) {
+      if (event.target == browser.contentDocument) {
+        browser.removeEventListener("load", onLoad, true);
+        resolve();
+      }
+    }, true);
+  });
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/browser/testFile.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Test Save as PDF</title>
+  </head>
+  <body>
+    <p>Save me as a PDF!</p>
+  </body>
+</html>
--- a/toolkit/components/jsdownloads/test/unit/test_DownloadStore.js
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadStore.js
@@ -52,23 +52,35 @@ add_task(function test_save_reload()
 
   listForSave.add(yield promiseNewDownload(httpUrl("source.txt")));
   listForSave.add(yield Downloads.createDownload({
     source: { url: httpUrl("empty.txt"),
               referrer: TEST_REFERRER_URL },
     target: getTempFile(TEST_TARGET_FILE_NAME),
   }));
 
+  // This PDF download should not be serialized because it never succeeds.
+  let pdfDownload = yield Downloads.createDownload({
+    source: { url: httpUrl("empty.txt"),
+              referrer: TEST_REFERRER_URL },
+    target: getTempFile(TEST_TARGET_FILE_NAME),
+    saver: "pdf",
+  });
+  listForSave.add(pdfDownload);
+
   let legacyDownload = yield promiseStartLegacyDownload();
   yield legacyDownload.cancel();
   listForSave.add(legacyDownload);
 
   yield storeForSave.save();
   yield storeForLoad.load();
 
+  // Remove the PDF download because it should not appear in this list.
+  listForSave.remove(pdfDownload);
+
   let itemsForSave = yield listForSave.getAll();
   let itemsForLoad = yield listForLoad.getAll();
 
   do_check_eq(itemsForSave.length, itemsForLoad.length);
 
   // Downloads should be reloaded in the same order.
   for (let i = 0; i < itemsForSave.length; i++) {
     // The reloaded downloads are different objects.
--- a/toolkit/components/jsdownloads/test/unit/test_Downloads.js
+++ b/toolkit/components/jsdownloads/test/unit/test_Downloads.js
@@ -56,16 +56,41 @@ add_task(function test_createDownload_pu
     source: { url: "about:blank" },
     target: { path: tempPath },
     saver: { type: "copy" }
   });
   do_check_false(download.source.isPrivate);
 });
 
 /**
+ * Tests createDownload for a pdf saver throws if only given a url.
+ */
+add_task(function test_createDownload_pdf()
+{
+  let download = yield Downloads.createDownload({
+    source: { url: "about:blank" },
+    target: { path: getTempFile(TEST_TARGET_FILE_NAME).path },
+    saver: { type: "pdf" },
+  });
+
+  try {
+    yield download.start();
+    do_throw("The download should have failed.");
+  } catch (ex if ex instanceof Downloads.Error && ex.becauseSourceFailed) { }
+
+  do_check_false(download.succeeded);
+  do_check_true(download.stopped);
+  do_check_false(download.canceled);
+  do_check_true(download.error !== null);
+  do_check_true(download.error.becauseSourceFailed);
+  do_check_false(download.error.becauseTargetFailed);
+  do_check_false(yield OS.File.exists(download.target.path));
+});
+
+/**
  * Tests "fetch" with nsIURI and nsIFile as arguments.
  */
 add_task(function test_fetch_uri_file_arguments()
 {
   let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
   yield Downloads.fetch(NetUtil.newURI(httpUrl("source.txt")), targetFile);
   yield promiseVerifyContents(targetFile.path, TEST_DATA_SHORT);
 });
--- a/toolkit/components/places/Database.cpp
+++ b/toolkit/components/places/Database.cpp
@@ -264,17 +264,17 @@ CreateRoot(nsCOMPtr<mozIStorageConnectio
 
   // The position of the new item in its folder.
   static int32_t itemPosition = 0;
 
   // A single creation timestamp for all roots so that the root folder's
   // last modification time isn't earlier than its childrens' creation time.
   static PRTime timestamp = 0;
   if (!timestamp)
-    timestamp = PR_Now();
+    timestamp = RoundedPRNow();
 
   // Create a new bookmark folder for the root.
   nsCOMPtr<mozIStorageStatement> stmt;
   nsresult rv = aDBConn->CreateStatement(NS_LITERAL_CSTRING(
     "INSERT INTO moz_bookmarks "
       "(type, position, title, dateAdded, lastModified, guid, parent) "
     "VALUES (:item_type, :item_position, :item_title,"
             ":date_added, :last_modified, :guid,"
@@ -723,16 +723,23 @@ Database::InitSchema(bool* aDatabaseMigr
 
       if (currentSchemaVersion < 25) {
         rv = MigrateV25Up();
         NS_ENSURE_SUCCESS(rv, rv);
       }
 
       // Firefox 36 uses schema version 25.
 
+      if (currentSchemaVersion < 26) {
+        rv = MigrateV26Up();
+        NS_ENSURE_SUCCESS(rv, rv);
+      }
+
+      // Firefox 37 uses schema version 26.
+
       // Schema Upgrades must add migration code here.
 
       rv = UpdateBookmarkRootTitles();
       // We don't want a broken localization to cause us to think
       // the database is corrupt and needs to be replaced.
       MOZ_ASSERT(NS_SUCCEEDED(rv));
     }
   }
@@ -1462,16 +1469,29 @@ Database::MigrateV25Up()
 
     rv = stmt->Execute();
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   return NS_OK;
 }
 
+nsresult
+Database::MigrateV26Up() {
+  MOZ_ASSERT(NS_IsMainThread());
+
+  // Round down dateAdded and lastModified values to milliseconds precision.
+  nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+    "UPDATE moz_bookmarks SET dateAdded = dateAdded - dateAdded % 1000, "
+    "                         lastModified = lastModified - lastModified % 1000"));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  return NS_OK;
+}
+
 void
 Database::Shutdown()
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(!mShuttingDown);
   MOZ_ASSERT(!mClosed);
 
   mShuttingDown = true;
@@ -1575,16 +1595,32 @@ Database::Observe(nsISupports *aSubject,
         "FROM moz_favicons "
         "WHERE guid IS NULL "
       ), getter_AddRefs(stmt));
       NS_ENSURE_SUCCESS(rv, rv);
       rv = stmt->ExecuteStep(&haveNullGuids);
       NS_ENSURE_SUCCESS(rv, rv);
       MOZ_ASSERT(!haveNullGuids && "Found a favicon without a GUID!");
     }
+
+    { // Sanity check for unrounded dateAdded and lastModified values (bug
+      // 1107308).
+      bool hasUnroundedDates = false;
+      nsCOMPtr<mozIStorageStatement> stmt;
+
+      nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+        "SELECT 1 "
+        "FROM moz_bookmarks "
+        "WHERE dateAdded % 1000 > 0 OR lastModified % 1000 > 0 LIMIT 1"
+      ), getter_AddRefs(stmt));
+      NS_ENSURE_SUCCESS(rv, rv);
+      rv = stmt->ExecuteStep(&hasUnroundedDates);
+      NS_ENSURE_SUCCESS(rv, rv);
+      MOZ_ASSERT(!hasUnroundedDates && "Found unrounded dates!");
+    }
 #endif
 
     // As the last step in the shutdown path, finalize the database handle.
     Shutdown();
   }
 
   return NS_OK;
 }
--- a/toolkit/components/places/Database.h
+++ b/toolkit/components/places/Database.h
@@ -11,17 +11,17 @@
 #include "nsIObserver.h"
 #include "mozilla/storage.h"
 #include "mozilla/storage/StatementCache.h"
 #include "mozilla/Attributes.h"
 #include "nsIEventTarget.h"
 
 // This is the schema version. Update it at any schema change and add a
 // corresponding migrateVxx method below.
-#define DATABASE_SCHEMA_VERSION 25
+#define DATABASE_SCHEMA_VERSION 26
 
 // Fired after Places inited.
 #define TOPIC_PLACES_INIT_COMPLETE "places-init-complete"
 // Fired when initialization fails due to a locked database.
 #define TOPIC_DATABASE_LOCKED "places-database-locked"
 // This topic is received when the profile is about to be lost.  Places does
 // initial shutdown work and notifies TOPIC_PLACES_SHUTDOWN to all listeners.
 // Any shutdown work that requires the Places APIs should happen here.
@@ -268,16 +268,17 @@ protected:
   nsresult MigrateV18Up();
   nsresult MigrateV19Up();
   nsresult MigrateV20Up();
   nsresult MigrateV21Up();
   nsresult MigrateV22Up();
   nsresult MigrateV23Up();
   nsresult MigrateV24Up();
   nsresult MigrateV25Up();
+  nsresult MigrateV26Up();
 
   nsresult UpdateBookmarkRootTitles();
 
 private:
   ~Database();
 
   /**
    * Singleton getter, invoked by class instantiation.
--- a/toolkit/components/places/Helpers.cpp
+++ b/toolkit/components/places/Helpers.cpp
@@ -316,16 +316,26 @@ void
 TruncateTitle(const nsACString& aTitle, nsACString& aTrimmed)
 {
   aTrimmed = aTitle;
   if (aTitle.Length() > TITLE_LENGTH_MAX) {
     aTrimmed = StringHead(aTitle, TITLE_LENGTH_MAX);
   }
 }
 
+PRTime
+RoundToMilliseconds(PRTime aTime) {
+  return aTime - (aTime % PR_USEC_PER_MSEC);
+}
+
+PRTime
+RoundedPRNow() {
+  return RoundToMilliseconds(PR_Now());
+}
+
 void
 ForceWALCheckpoint()
 {
   nsRefPtr<Database> DB = Database::GetDatabase();
   if (DB) {
     nsCOMPtr<mozIStorageAsyncStatement> stmt = DB->GetAsyncStatement(
       "pragma wal_checkpoint "
     );
--- a/toolkit/components/places/Helpers.h
+++ b/toolkit/components/places/Helpers.h
@@ -9,16 +9,17 @@
 /**
  * This file contains helper classes used by various bits of Places code.
  */
 
 #include "mozilla/storage.h"
 #include "nsIURI.h"
 #include "nsThreadUtils.h"
 #include "nsProxyRelease.h"
+#include "prtime.h"
 #include "mozilla/Telemetry.h"
 
 namespace mozilla {
 namespace places {
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Asynchronous Statement Callback Helper
 
@@ -144,16 +145,32 @@ bool IsValidGUID(const nsACString& aGUID
  * @param aTitle
  *        The title to truncate (if necessary)
  * @param aTrimmed
  *        Output parameter to return the trimmed string
  */
 void TruncateTitle(const nsACString& aTitle, nsACString& aTrimmed);
 
 /**
+ * Round down a PRTime value to milliseconds precision (...000).
+ *
+ * @param aTime
+ *        a PRTime value.
+ * @return aTime rounded down to milliseconds precision.
+ */
+PRTime RoundToMilliseconds(PRTime aTime);
+
+/**
+ * Round down PR_Now() to milliseconds precision.
+ *
+ * @return @see PR_Now, RoundToMilliseconds.
+ */
+PRTime RoundedPRNow();
+
+/**
  * Used to finalize a statementCache on a specified thread.
  */
 template<typename StatementType>
 class FinalizeStatementCacheProxy : public nsRunnable
 {
 public:
   /**
    * Constructor.
--- a/toolkit/components/places/nsAnnotationService.cpp
+++ b/toolkit/components/places/nsAnnotationService.cpp
@@ -1912,32 +1912,32 @@ nsAnnotationService::StartSetAnnotation(
     rv = aStatement->BindInt64ByName(NS_LITERAL_CSTRING("id"), oldAnnoId);
     NS_ENSURE_SUCCESS(rv, rv);
     rv = aStatement->BindInt64ByName(NS_LITERAL_CSTRING("date_added"), oldAnnoDate);
     NS_ENSURE_SUCCESS(rv, rv);
   }
   else {
     rv = aStatement->BindNullByName(NS_LITERAL_CSTRING("id"));
     NS_ENSURE_SUCCESS(rv, rv);
-    rv = aStatement->BindInt64ByName(NS_LITERAL_CSTRING("date_added"), PR_Now());
+    rv = aStatement->BindInt64ByName(NS_LITERAL_CSTRING("date_added"), RoundedPRNow());
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   rv = aStatement->BindInt64ByName(NS_LITERAL_CSTRING("fk"), fkId);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = aStatement->BindInt64ByName(NS_LITERAL_CSTRING("name_id"), nameID);
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = aStatement->BindInt32ByName(NS_LITERAL_CSTRING("flags"), aFlags);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = aStatement->BindInt32ByName(NS_LITERAL_CSTRING("expiration"), aExpiration);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = aStatement->BindInt32ByName(NS_LITERAL_CSTRING("type"), aType);
   NS_ENSURE_SUCCESS(rv, rv);
-  rv = aStatement->BindInt64ByName(NS_LITERAL_CSTRING("last_modified"), PR_Now());
+  rv = aStatement->BindInt64ByName(NS_LITERAL_CSTRING("last_modified"), RoundedPRNow());
   NS_ENSURE_SUCCESS(rv, rv);
 
   // On success, leave the statement open, the caller will set the value
   // and execute the statement.
   setAnnoScoper.Abandon();
 
   return NS_OK;
 }
--- a/toolkit/components/places/nsINavBookmarksService.idl
+++ b/toolkit/components/places/nsINavBookmarksService.idl
@@ -403,37 +403,61 @@ interface nsINavBookmarksService : nsISu
    *  @param aItemId
    *         The id of the item whose title should be retrieved
    *  @return The title of the item.
    */
   AUTF8String getItemTitle(in long long aItemId);
 
   /**
    * Set the date added time for an item.
+   *
+   * @param aItemId
+   *        the id of the item whose date added time should be updated.
+   * @param aDateAdded
+   *        the new date added value in microseconds.  Note that it is rounded
+   *        down to milliseconds precision.
    */
   void setItemDateAdded(in long long aItemId, in PRTime aDateAdded);
+
   /**
    * Get the date added time for an item.
+   *
+   * @param aItemId
+   *        the id of the item whose date added time should be retrieved.
+   *
+   * @return the date added value in microseconds.
    */
   PRTime getItemDateAdded(in long long aItemId);
 
   /**
    * Set the last modified time for an item.
    *
-   *  @note This is the only method that will send an itemChanged notification
-   *        for the property.  lastModified will still be updated in
-   *        any other method that changes an item property, but we will send
-   *        the corresponding itemChanged notification instead.
+   * @param aItemId
+   *        the id of the item whose last modified time should be updated.
+   * @param aLastModified
+   *        the new last modified value in microseconds.  Note that it is
+   *        rounded down to milliseconds precision.
+   *
+   * @note This is the only method that will send an itemChanged notification
+   *       for the property.  lastModified will still be updated in
+   *       any other method that changes an item property, but we will send
+   *       the corresponding itemChanged notification instead.
    */
   void setItemLastModified(in long long aItemId, in PRTime aLastModified);
+
   /**
    * Get the last modified time for an item.
    *
-   *  @note When an item is added lastModified is set to the same value as
-   *        dateAdded.
+   * @param aItemId
+   *        the id of the item whose last modified time should be retrieved.
+   *
+   * @return the date added value in microseconds.
+   *
+   * @note When an item is added lastModified is set to the same value as
+   *       dateAdded.
    */
   PRTime getItemLastModified(in long long aItemId);
 
   /**
    * Get the URI for a bookmark item.
    */
   nsIURI getBookmarkURI(in long long aItemId);
 
--- a/toolkit/components/places/nsNavBookmarks.cpp
+++ b/toolkit/components/places/nsNavBookmarks.cpp
@@ -521,17 +521,17 @@ nsNavBookmarks::InsertBookmark(int64_t a
   else {
     index = aIndex;
     // Create space for the insertion.
     rv = AdjustIndices(aFolder, index, INT32_MAX, 1);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   *aNewBookmarkId = -1;
-  PRTime dateAdded = PR_Now();
+  PRTime dateAdded = RoundedPRNow();
   nsAutoCString guid(aGUID);
   nsCString title;
   TruncateTitle(aTitle, title);
 
   rv = InsertBookmarkInDB(placeId, BOOKMARK, aFolder, index, title, dateAdded,
                           0, folderGuid, grandParentId, aURI,
                           aNewBookmarkId, guid);
   NS_ENSURE_SUCCESS(rv, rv);
@@ -623,17 +623,17 @@ nsNavBookmarks::RemoveItem(int64_t aItem
 
   // Fix indices in the parent.
   if (bookmark.position != DEFAULT_INDEX) {
     rv = AdjustIndices(bookmark.parentId,
                        bookmark.position + 1, INT32_MAX, -1);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
-  bookmark.lastModified = PR_Now();
+  bookmark.lastModified = RoundedPRNow();
   rv = SetItemDateInternal(LAST_MODIFIED, bookmark.parentId,
                            bookmark.lastModified);
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = transaction.Commit();
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsCOMPtr<nsIURI> uri;
@@ -751,17 +751,17 @@ nsNavBookmarks::CreateContainerWithID(in
   } else {
     index = *aIndex;
     // Create space for the insertion.
     rv = AdjustIndices(aParent, index, INT32_MAX, 1);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   *aNewFolder = aItemId;
-  PRTime dateAdded = PR_Now();
+  PRTime dateAdded = RoundedPRNow();
   nsAutoCString guid(aGUID);
   nsCString title;
   TruncateTitle(aTitle, title);
 
   rv = InsertBookmarkInDB(-1, FOLDER, aParent, index,
                           title, dateAdded, 0, folderGuid, grandParentId,
                           nullptr, aNewFolder, guid);
   NS_ENSURE_SUCCESS(rv, rv);
@@ -812,17 +812,17 @@ nsNavBookmarks::InsertSeparator(int64_t 
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   *aNewItemId = -1;
   // Set a NULL title rather than an empty string.
   nsCString voidString;
   voidString.SetIsVoid(true);
   nsAutoCString guid(aGUID);
-  PRTime dateAdded = PR_Now();
+  PRTime dateAdded = RoundedPRNow();
   rv = InsertBookmarkInDB(-1, SEPARATOR, aParent, index, voidString, dateAdded,
                           0, folderGuid, grandParentId, nullptr,
                           aNewItemId, guid);
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = transaction.Commit();
   NS_ENSURE_SUCCESS(rv, rv);
 
@@ -1100,17 +1100,17 @@ nsNavBookmarks::RemoveFolderChildren(int
       "DELETE FROM moz_items_annos "
       "WHERE id IN ("
         "SELECT a.id from moz_items_annos a "
         "LEFT JOIN moz_bookmarks b ON a.item_id = b.id "
         "WHERE b.id ISNULL)"));
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Set the lastModified date.
-  rv = SetItemDateInternal(LAST_MODIFIED, folder.id, PR_Now());
+  rv = SetItemDateInternal(LAST_MODIFIED, folder.id, RoundedPRNow());
   NS_ENSURE_SUCCESS(rv, rv);
 
   for (uint32_t i = 0; i < folderChildrenArray.Length(); i++) {
     BookmarkData& child = folderChildrenArray[i];
     if (child.type == TYPE_BOOKMARK) {
       // If not a tag, recalculate frecency for this entry, since it changed.
       if (child.grandParentId != mTagsRoot) {
         nsNavHistory* history = nsNavHistory::GetHistoryService();
@@ -1284,17 +1284,17 @@ nsNavBookmarks::MoveItem(int64_t aItemId
     rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("item_index"), newIndex);
     NS_ENSURE_SUCCESS(rv, rv);
     rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), bookmark.id);
     NS_ENSURE_SUCCESS(rv, rv);
     rv = stmt->Execute();
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
-  PRTime now = PR_Now();
+  PRTime now = RoundedPRNow();
   rv = SetItemDateInternal(LAST_MODIFIED, bookmark.parentId, now);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = SetItemDateInternal(LAST_MODIFIED, aNewParent, now);
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = transaction.Commit();
   NS_ENSURE_SUCCESS(rv, rv);
 
@@ -1381,16 +1381,18 @@ nsNavBookmarks::FetchItemInfo(int64_t aI
   return NS_OK;
 }
 
 nsresult
 nsNavBookmarks::SetItemDateInternal(enum BookmarkDate aDateType,
                                     int64_t aItemId,
                                     PRTime aValue)
 {
+  aValue = RoundToMilliseconds(aValue);
+
   nsCOMPtr<mozIStorageStatement> stmt;
   if (aDateType == DATE_ADDED) {
     // lastModified is set to the same value as dateAdded.  We do this for
     // performance reasons, since it will allow us to use an index to sort items
     // by date.
     stmt = mDB->GetStatement(
       "UPDATE moz_bookmarks SET dateAdded = :date, lastModified = :date "
       "WHERE id = :item_id"
@@ -1422,17 +1424,19 @@ nsNavBookmarks::SetItemDateInternal(enum
 NS_IMETHODIMP
 nsNavBookmarks::SetItemDateAdded(int64_t aItemId, PRTime aDateAdded)
 {
   NS_ENSURE_ARG_MIN(aItemId, 1);
 
   BookmarkData bookmark;
   nsresult rv = FetchItemInfo(aItemId, bookmark);
   NS_ENSURE_SUCCESS(rv, rv);
-  bookmark.dateAdded = aDateAdded;
+
+  // Round here so that we notify with the right value.
+  bookmark.dateAdded = RoundToMilliseconds(aDateAdded);
 
   rv = SetItemDateInternal(DATE_ADDED, bookmark.id, bookmark.dateAdded);
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Note: mDBSetItemDateAdded also sets lastModified to aDateAdded.
   NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
                    nsINavBookmarkObserver,
                    OnItemChanged(bookmark.id,
@@ -1466,17 +1470,19 @@ nsNavBookmarks::GetItemDateAdded(int64_t
 NS_IMETHODIMP
 nsNavBookmarks::SetItemLastModified(int64_t aItemId, PRTime aLastModified)
 {
   NS_ENSURE_ARG_MIN(aItemId, 1);
 
   BookmarkData bookmark;
   nsresult rv = FetchItemInfo(aItemId, bookmark);
   NS_ENSURE_SUCCESS(rv, rv);
-  bookmark.lastModified = aLastModified;
+
+  // Round here so that we notify with the right value.
+  bookmark.lastModified = RoundToMilliseconds(aLastModified);
 
   rv = SetItemDateInternal(LAST_MODIFIED, bookmark.id, bookmark.lastModified);
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Note: mDBSetItemDateAdded also sets lastModified to aDateAdded.
   NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
                    nsINavBookmarkObserver,
                    OnItemChanged(bookmark.id,
@@ -1530,17 +1536,17 @@ nsNavBookmarks::SetItemTitle(int64_t aIt
   if (title.IsVoid()) {
     rv = statement->BindNullByName(NS_LITERAL_CSTRING("item_title"));
   }
   else {
     rv = statement->BindUTF8StringByName(NS_LITERAL_CSTRING("item_title"),
                                          title);
   }
   NS_ENSURE_SUCCESS(rv, rv);
-  bookmark.lastModified = PR_Now();
+  bookmark.lastModified = RoundToMilliseconds(RoundedPRNow());
   rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("date"),
                                   bookmark.lastModified);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), bookmark.id);
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = statement->Execute();
   NS_ENSURE_SUCCESS(rv, rv);
@@ -1999,17 +2005,17 @@ nsNavBookmarks::ChangeBookmarkURI(int64_
     "UPDATE moz_bookmarks SET fk = :page_id, lastModified = :date "
     "WHERE id = :item_id "
   );
   NS_ENSURE_STATE(statement);
   mozStorageStatementScoper scoper(statement);
 
   rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), newPlaceId);
   NS_ENSURE_SUCCESS(rv, rv);
-  bookmark.lastModified = PR_Now();
+  bookmark.lastModified = RoundedPRNow();
   rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("date"),
                                   bookmark.lastModified);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = statement->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), bookmark.id);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = statement->Execute();
   NS_ENSURE_SUCCESS(rv, rv);
 
@@ -2355,17 +2361,17 @@ nsNavBookmarks::SetKeywordForBookmark(in
 
     // Add new keyword association to the hash, removing the old one if needed.
     if (!oldKeyword.IsEmpty())
       mBookmarkToKeywordHash.Remove(bookmark.id);
     mBookmarkToKeywordHash.Put(bookmark.id, keyword);
     rv = updateBookmarkStmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), keyword);
   }
   NS_ENSURE_SUCCESS(rv, rv);
-  bookmark.lastModified = PR_Now();
+  bookmark.lastModified = RoundedPRNow();
   rv = updateBookmarkStmt->BindInt64ByName(NS_LITERAL_CSTRING("date"),
                                            bookmark.lastModified);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = updateBookmarkStmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"),
                                            bookmark.id);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = updateBookmarkStmt->Execute();
   NS_ENSURE_SUCCESS(rv, rv);
@@ -2830,17 +2836,17 @@ nsNavBookmarks::OnPageAnnotationSet(nsIU
 
 NS_IMETHODIMP
 nsNavBookmarks::OnItemAnnotationSet(int64_t aItemId, const nsACString& aName)
 {
   BookmarkData bookmark;
   nsresult rv = FetchItemInfo(aItemId, bookmark);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  bookmark.lastModified = PR_Now();
+  bookmark.lastModified = RoundedPRNow();
   rv = SetItemDateInternal(LAST_MODIFIED, bookmark.id, bookmark.lastModified);
   NS_ENSURE_SUCCESS(rv, rv);
 
   NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
                    nsINavBookmarkObserver,
                    OnItemChanged(bookmark.id,
                                  aName,
                                  true,
--- a/toolkit/components/places/nsNavHistoryResult.cpp
+++ b/toolkit/components/places/nsNavHistoryResult.cpp
@@ -3915,17 +3915,17 @@ nsNavHistoryFolderResultNode::OnItemMove
       NS_ENSURE_SUCCESS(rv, rv);
     }
     if (aOldParent == mTargetFolderItemId) {
       OnItemRemoved(aItemId, aOldParent, aOldIndex, aItemType, itemURI,
                     aGUID, aOldParentGUID);
     }
     if (aNewParent == mTargetFolderItemId) {
       OnItemAdded(aItemId, aNewParent, aNewIndex, aItemType, itemURI, itemTitle,
-                  PR_Now(), // This is a dummy dateAdded, not the real value.
+                  RoundedPRNow(), // This is a dummy dateAdded, not the real value.
                   aGUID, aNewParentGUID);
     }
   }
   return NS_OK;
 }
 
 
 /**
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks.js
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks.js
@@ -144,18 +144,20 @@ add_task(function test_bookmarks() {
   do_check_eq(lastModified, dateAdded);
 
   // The time before we set the title, in microseconds.
   let beforeSetTitle = Date.now() * 1000;
   do_check_true(beforeSetTitle >= beforeInsert);
 
   // Workaround possible VM timers issues moving lastModified and dateAdded
   // to the past.
-  bs.setItemLastModified(newId, --lastModified);
-  bs.setItemDateAdded(newId, --dateAdded);
+  lastModified -= 1000;
+  bs.setItemLastModified(newId, lastModified);
+  dateAdded -= 1000;
+  bs.setItemDateAdded(newId, dateAdded);
 
   // set bookmark title
   bs.setItemTitle(newId, "Google");
   do_check_eq(bookmarksObserver._itemChangedId, newId);
   do_check_eq(bookmarksObserver._itemChangedProperty, "title");
   do_check_eq(bookmarksObserver._itemChangedValue, "Google");
 
   // check that dateAdded hasn't changed
@@ -322,18 +324,20 @@ add_task(function test_bookmarks() {
   try {
     let dateAdded = bs.getItemDateAdded(kwTestItemId);
     // after just inserting, modified should not be set
     let lastModified = bs.getItemLastModified(kwTestItemId);
     do_check_eq(lastModified, dateAdded);
 
     // Workaround possible VM timers issues moving lastModified and dateAdded
     // to the past.
+    lastModified -= 1000;
     bs.setItemLastModified(kwTestItemId, --lastModified);
-    bs.setItemDateAdded(kwTestItemId, --dateAdded);
+    dateAdded -= 1000;
+    bs.setItemDateAdded(kwTestItemId, dateAdded);
 
     bs.setKeywordForBookmark(kwTestItemId, "bar");
 
     let lastModified2 = bs.getItemLastModified(kwTestItemId);
     LOG("test setKeywordForBookmark");
     LOG("dateAdded = " + dateAdded);
     LOG("lastModified = " + lastModified);
     LOG("lastModified2 = " + lastModified2);
@@ -449,18 +453,20 @@ add_task(function test_bookmarks() {
                                   bs.DEFAULT_INDEX, "");
   dateAdded = bs.getItemDateAdded(newId10);
   // after just inserting, modified should not be set
   lastModified = bs.getItemLastModified(newId10);
   do_check_eq(lastModified, dateAdded);
 
   // Workaround possible VM timers issues moving lastModified and dateAdded
   // to the past.
-  bs.setItemLastModified(newId10, --lastModified);
-  bs.setItemDateAdded(newId10, --dateAdded);
+  lastModified -= 1000;
+  bs.setItemLastModified(newId10, lastModified);
+  dateAdded -= 1000;
+  bs.setItemDateAdded(newId10, dateAdded);
 
   bs.changeBookmarkURI(newId10, uri("http://foo11.com/"));
 
   // check that lastModified is set after we change the bookmark uri
   lastModified2 = bs.getItemLastModified(newId10);
   LOG("test changeBookmarkURI");
   LOG("dateAdded = " + dateAdded);
   LOG("lastModified = " + lastModified);
@@ -591,22 +597,22 @@ add_task(function test_bookmarks() {
   }
 
   // check setItemLastModified() and setItemDateAdded()
   let newId14 = bs.insertBookmark(testRoot, uri("http://bar.tld/"),
                                   bs.DEFAULT_INDEX, "");
   dateAdded = bs.getItemDateAdded(newId14);
   lastModified = bs.getItemLastModified(newId14);
   do_check_eq(lastModified, dateAdded);
-  bs.setItemLastModified(newId14, 1234);
+  bs.setItemLastModified(newId14, 1234000000000000);
   let fakeLastModified = bs.getItemLastModified(newId14);
-  do_check_eq(fakeLastModified, 1234);
-  bs.setItemDateAdded(newId14, 4321);
+  do_check_eq(fakeLastModified, 1234000000000000);
+  bs.setItemDateAdded(newId14, 4321000000000000);
   let fakeDateAdded = bs.getItemDateAdded(newId14);
-  do_check_eq(fakeDateAdded, 4321);
+  do_check_eq(fakeDateAdded, 4321000000000000);
   
   // ensure that removing an item removes its annotations
   do_check_true(anno.itemHasAnnotation(newId3, "test-annotation"));
   bs.removeItem(newId3);
   do_check_false(anno.itemHasAnnotation(newId3, "test-annotation"));
 
   // bug 378820
   let uri1 = uri("http://foo.tld/a");
--- a/toolkit/components/places/tests/head_common.js
+++ b/toolkit/components/places/tests/head_common.js
@@ -1,14 +1,14 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-const CURRENT_SCHEMA_VERSION = 25;
+const CURRENT_SCHEMA_VERSION = 26;
 const FIRST_UPGRADABLE_SCHEMA_VERSION = 11;
 
 const NS_APP_USER_PROFILE_50_DIR = "ProfD";
 const NS_APP_PROFILE_DIR_STARTUP = "ProfDS";
 
 // Shortcuts to transitions type.
 const TRANSITION_LINK = Ci.nsINavHistoryService.TRANSITION_LINK;
 const TRANSITION_TYPED = Ci.nsINavHistoryService.TRANSITION_TYPED;
index ef5e1def9b7f5a5c6495a5c3ac5eb76cb6a6038b..2afd1da1fd7ce40cda051975bb26d4a3223ff4b8
GIT binary patch
literal 1179648
zc%1Fsdz@TleK7F3?PWIEgp0U<76v3hN;Z2-NVr7YkPYO1T_C9MGTEKoOftK(nVHRQ
z7Lt=hR8aaBTWz(qg72${tyb&hZ50qHRJ@?&tyI)1-ms+tFJ7vEh47x4-A#6L0qm!r
z4_>}snDd<H_gv32&pC7UuWVSiGF!@|dh><;bSc#q*%OJyBCkoMB9TZu{G1Uv=#=o2
zn3Ru(pOYfbFCJ<9*P8lWpNPCFcY0*^zFmKJ&8K%2um1WqZyvpS=TAqk*?GavPmTQh
z@cxmH4xcgnw?khm4VQkucw2E};gQ15?N4v-8vMrKhWvN)>47f~T+#pC{@3Qd5OxCq
z0000000000000000000000000000000Kdlep7E-hhIQve_ioA#^khc*^P^qaQl`Jy
zl^z(#7c1xKQmK&LJXFdYsm2)>ukUEz*pcd7y=+})M{3#XB^_6!UbvUkn$?HaN}XNK
zbY*+yq{{iO0}aB^?7cHjuW7jC)ac%h1Cto*dN7yn&Ky3&O5v-HHn*cTotW16geo<T
zHlx^SH4V$oitat_z@VG+`EC8_!nR^p-%z%vYiJ<5eJFEy>FGzC*3sKf%<Djn)Y;`K
zvv;Iw8ZJI9y7z<wqg1AR<Oz%wop!X*kJ55t=&|DQaf_$cG_;4~9((AxLxtRthfTe}
zu!l4}bkwkT_TFPpscF~{uK4^Ttay3v<kH1bf4(Q%o6YnbUggyB860W7kJ$PMi{86!
zPO4mQ)p)(xyMC{x;i98#g28klGf+Cb;FRZ?$fPz$7=EmDPHHe;%$BnGf!W(%S<`Sq
zIDGw4R^;&T@4dh(Jl~a<b`EA{?~T7A+;dHnFL_J0Sjrc6?#PC-Dt7gxC*6lrie7ox
z*hgtOG5(a|)Y+A^QY)Or$<IHF-a@|r$VIPsfms}Ad6Zd{%eyL>*?SvKs%cmi&f=n@
z%%ZY=kG>!$zra+cHb2UI4nCwNt<UZg!^h=mlb2`wJ}d9uO1&ww8Y?{Mu<0DN(V_Qe
zxklHd@%tNU8Wx4)Cl4LJw~*=140P{2B!1%2hMrRY&`~D}XK#IZO~ZmC4Of{`cYbJ~
zbTHU(w6P9WK6Ie5E2|XE&YV!wFh3kCdgxeN^2O32v6mlhr~`Em9jTDnQ7N3=|FZDb
z4u_d`*b1h1WV`bNhlEc!>H<!xe%Shrm6d<aRvcg5Fl$!ywWajtT&B1^7d{KRilua^
zW%BvU#s}HhzIbIvYI4chrOmlH*`AR(<=pIjuRE@$;hb}#*XAqT9r8neNcQ+CogVg5
zkV<8H!mHZYaY@Je)Y|pSR<*C+lv>fTX-;bVb>Tqa>NOivt2<Y&Ji@Q_M4dUQ{%n7y
ztNb8KUDm$-;!E4tpWW6vJGG=^X?y3&iRR&wl)~q1YUP^MmyFk%lj_Z-`-%s~nrP5G
zCzTl)%oft+$MKO$D?=S2UcO7(dwMcGM=G5B6LWa^?0qYat!X&*)abRD1Iv40VJj!c
z9k!$|xR?`9k>9=5+<VN-nufNv=-x}FtmeThE*>Ft%n?_3QibPR>w$Ft)CC)VDVkXF
z*&WAJH*9K)Mk+V$_-&RRD&;Hrt|N@r)pCU3v3r~9YsxqEzS9m|+4vn_oP0jxuxptp
zdA^GX-^eC^rQ_F7`6|+>#Vgk=KFaQx@)egmn3F2MQl>U`T(NQXf*I8fD`q|4g-=Xv
z?20FHGuKY9E<bQKO?iA}!netR?nATnM|_Bl7oJ`IZsvr!+1J!n)^6_w2i9&|X6JCe
z&{LdzKJBozn<zQ@+NIh%H?CQ>I&8VBWA(;4sqq$-6&PQ}*$bw<&_Ya%Gqw;Dx#@dr
zYR7i-`~&ko_<~o+=S#&Sgp!9{#UT~S7gIjznzD6U&YgYWYwykHdNPGc4~N+u$rswh
zhYT`yC5Hs-_8nJK({R>V(KmD-enC?*wMV~*=iA4VA1UEwY&+};G4>Q2d-Xn8apD#H
zV5F2S<uX&Ba^v61lt<?HLRE$>f8$4ZcpdqXHTgc-JEyAh3$X931B+6AX(%2zsXlBm
zD&gl_h;oIoJvi|kI{4*|zh_Ol*l_xX|5i+X>JAn%z1fkm>9$l?H*}o(d|RP1yRi*W
zIjz}yZoH<Uu`#;u)Pt`O1A{}Q@t5C2^NGV>A|{oVFZ+3Z;3`$8Jj@Q34HYwE?|lca
zee$h5>FvAH%k0?~C8`@XG(O+8PM+D=B~Ol2MIw>J-b-RN4NXnaeTxo$zmwnk4-3Q(
z|GuaGm2mX${ovm-lXmwZi#7Rgi$h~mZ=hnPSe$fQ%-(l(w5H*-)1q%|KX4^uZ=VOx
zVu#Ir;{7!BqJMslc>7JfOHYkXdY3=a+rpM?cq<;^<zv#Gn7a9oRO8@_^`smBkeld0
z!^W0LEf4ua>Okd5cYpaAQpjZc299<S2VW%W!_749>W3r2`-4@xpV<A*-Ai{LyKDEZ
z_G=!vCLe|Y000000000000000000000000000000000000N}UI-r8yPk*k+ACTnY>
z(dt>XTS}$D;`vQY!^6XK`}3pOTrNE~U+8Pf3^aCbXzI-tGQIhcrh#nVmQrqKIbSRl
zh6b9JjuoU<j}@ermJ3qrhXzt>(|wt~LVj*Km&=ak`{$NBu6iy~mE5}jv0I)VX}$cC
z($?a(ONZAi?yjqk<RgvAWNqa6W^&Xy96CzR*dVcMyB4=+HgB9)>d237+m@`41TQ?u
zU?IOX(_Jba7_Du=+~#!43k~+@sxy|YS<$(0Rd(aLt&4{*sI8Bzi$rR}!D=J3l9hj$
z{`~U4|GwzlGmH5`X;I6(GyBsc>odinT&cLIrFr<g{L+r)E4vz(G;Z2^OwEkQ-kpaw
z@6G3WGKEEp*Q{Bws(t;64PC1`R(GB`9_?JcblJ*|C0z$fCki&MS+jC+`}#At4`m8F
zH<oA6GCqUWsWUh;Gt!+K>dCAND`qocq<g{go(&5(t?itD+2w83^^x{NCsgTX;nZH1
zbdSy-SUI}0b8y|JwN>?z&PZczxWQs!qvzjX>CO3}(qW66h}<yTd~x$|>!$9mm948f
z6ZMg<7u#cZexOvyZXPOS4(m1^xuJR0CEJ#4>AQ00;86N?@%l*ri|uw|Uvv)@OZoon
z=;6J`A~!6)WX;wcmoHe?H(DxO7ORhRS9(vr_};f<a)XC;9J!&ZeRxUh#?kin?C91_
z(fUZX(r@Hfm~|#xlyvS0U0*zZc<Ayg7GxJMSi7!1d_Kg3dn3XB3;rcc0000000000
z00000000000000000000000000002sH`#IVnn*NS9j%Q`i&Q1I?tko-r>l;Q)kKoz
zSlw7Cc5T%$(V9qYv^rUvtb`t2bw+);^`4h__5c6?0000000000000000000000000
z0000000000@Di&lANIV&lK=n!00000000000000000000000000000000000fR|W(
z_=yIOM#BFA00000000000000000000000000000000000007{3!ST`R$g5(tk^cN>
zHkV7!%@_J2H`Ij%(cmYM@P7aR000000000000000000000000000000000000QlW8
zBmSyne||KZ%cbY$3w`xrXy!G47YY73cz1AB(7gMnyZ>(YHM?KEJG$%hyZ(6BYj?f!
znjeP600000000000000000000000000000000000006+Rv2$ilk3=KUXn0y(Ju4oI
zMXHip_dmF3`9!+u+?Jl!EBlvU+}3g4{29|D$!IkBs!HYB*!oO&W}uYXnC>f%RX^|c
zvGl^ubg_MKYe#1E%GT9wi|Q-QYTGK!YQ~%8cBU5R^V|B<g>9Lhv1aGa8cP@6zj~x=
z{?<#^u5G<+pyS->W6ft(npekH<%^}%W!Yl3G|{fLFqY15?Oivtq+|Qi*3QjWcC<~a
zw2S^;rCn7`ynS<isFdm)pX<wKj-@AV-&kl}*txQ2aCtVfctPD*qp`sfHL*)FrPQ*4
z9r@gjiP4T-IF=s&{!N2zncmLs9ShbhSaCseY;Bsx7P98zp<*fDpB>GlmSzi?-u%c|
zqv?M?md>2N{@lw3ms~z_X{L4gk_&6cF0rN3I979MCO4Q$5A>uq4wdqSY&thS=fwEX
zwLfayux|5??D9fxOR;!<&DeK%&RFYMxI&eGmwxKVvB<=A@7S?&{_0F=)xf~QwuRN<
z$jNAOtZh8J+ER9)FSVhRE=(-%*Pi|ASj)&??r6Mp!@!Ex)=fh@mtR;_9yJ*qpL4u(
zpqMYWIq<EH&H27t-!az9;|qrddb7DqPgmL8#8~SS#^-!s4^7k$(^44g8h3uYQh$7T
zV+Y1aZOrF$o706UZNm*w8Vgt2E{Ijy*3Le(?W)Yc(A0*DmiLFTi9u&a%MEL5>&F@%
zI=@0bUz(^IR*6&v_eO%B2ag611rLO$00000000000000000000000000000000000
z0002+n`&BB{1wrmf!=H`(^FNOh@TKGrTdDBY4I>z%I9;N(}j3#EDZN&28LqEX#Aw)
zU@qOADb|IJtAhI?!IQz`!NbAV!&3kN00000000000000000000000000000000000
z0QikIE&ht=&_HiCm+7f0pO^Ca+~#y4UK>9lT1xj76Uq2V$-!K@J5wx&`ZEJVvHEhu
zJumUZ0RR910000000000000000000000000000000002sC018H?0JbN0RR9100000
z000000000000000000000000000000FR}XaVb4oE2><{900000000000000000000
z000000000000000c!|}ApQ`P5M0Q;<`q7cvp{v4E0000000000000000000000000
z000000001h-yUyHRV5>PPq<;^)aqGVN~OW#`Atpz`O$o#Z*F(bz})aOo6Du=4rEGA
z{h6L@x+&Y=*VLOWWP0->O}*(I+3x(n-0*NwYjaCmWAno6*RH;#zV76@u<QC|mo8Z!
zrpJ^&wbkMA)So?<jznV5E?d&Rap50)_^Z2C-nadgH9!8tf1Q2)qnE8(GyMADUBM5t
z(?9jj|NDkZ&wG9LM;|+X)$4D`eE5AAe)7aKkIigb^r8FaH2$xRSG;!KZQJH{e{jRw
z3&XE}^V{Yf-}}oup1r$&)Bd0Bf9~0D{@Wiu`Q}H0vpzVq_U#Y6di|e#egD9cX9wSY
z`L=tXx~Jxe`~K|Q;`S{c{>rmAb*%Wz<)xmdpMChdg>NU$*;%{u4`#Fve<$^!$K$ap
zKUDjnwwpiw@h3K{di)>mUNWzH`P0`g{_cghzHQ|x7nBBD=U;UEC*M)G<b+eMz4w!^
z9$E0t@2y=>yYBi^uHIH^KKZr(cjq}zKlHMj+yDJDPrv^ycm2^_ca8pi%`L|zAO2L|
z_trgd-KNe(AN|N9Z|ZKZ`IA$(-t*M0Gj3{p<43pt+w9z)bMN~2$ig?KM=q+_c1q1x
zXV3m0J&EtH-+l7C>pn5~g>7$mbngX&D?as>x7_mQpMB({=@<R4pPhU2f@PcMfAG9_
zes$pu4-bx9-TtOOp1pkVq@f?*mwVHf9(wecTff+{;HR$~_{7G&&t37B&-A|QpQ|r?
z>j}?ZbH`indf)f1y6y)zHvFIJFKupXx#%;opLJ&5`QfwgdH4O--1oK*{NUcFI)B`K
z$^EbU)O*f4{esg^|EF{A`Qv!Y^riFP{FSS^9=J93(dOp*_x#JpK052Xs?~q~vD2=2
zSKUYc`S-86`lNj~_0IUmZQnjK*D$Yo*{AD|o&JWunf{TD({I^0<0Ea)UiHNv4(9)`
z>1AI$yY|`d-}t8=pV4)}w_4siyyU^{55D{rgFFA=j7#p^``$HoHNS83iOa9L^`TEB
zT0XP?m=$+_@APla_~7s{qaXQv^JP!$-T&2x{_byHzvF@GGwKqnhc6qsBe?x13y!<@
zs&9Syy{C;PfAQg;Uy^?6_pg{yJhAb{yWaif```EDe?2uf^sR-NX&tZr{$G6ft!Lc6
zJN|~#_N7yAN}v6nyKe10@oUXDyszz@e{|CH<;Q;Kl+({&@a;cq*mCXPfBeB+@B7Xt
z3vc?sY17}-KfUSnlYTke{2ynZ^rIhce)X4MzjoP@ukOER<>^<>nRVN@=Jcf>%v}G-
zwm-h<op;``@t(^YfALh|O`n>6_8Vusf5|8Q<hsW`5Z`~#d#+ou<P&eXZr}B1zx}${
zb(bz(68q(&Z`&O`@ju^s^LO`l{h#vPx2$9JlD}Sj)xK<hx-Zi-IM5fVN<R6I54`ye
zJ%g9dUtK7!>E5<<^o_F;;Vl}!{$=C0Xn2PX4-X%>HN#uAskOOzep7$CC(}4I*f@Sy
zHnlaj%xhV&psl&Nt+{pH{1<U&MiPDPtJ))xk9>YX)m#2O%%4>1xT3Tn-&-0^7c!Cd
zp8V!aYFT-%>oe(|o!h^VxiAumeyw~vk9>3UGtWJDQG8Y;wdb8Tp7+Fe?(BQwvB#eI
z@P|J1%y-`Lwx8U2{q<i6QqMj3qksOZzx~c_!)spt=g~;?o=72Dd((Nv&wcspUw`V(
zY4N+CYkSS@b=N%++kWyfx4q^kf7$t0JMMbm?&jA-_Fn(4w!T+apZtr9UiO#AF8R~=
zPlE2+`tP4Jd-k&piR<5cQskJ|OuxVRshMq&&p&nND;}=vzApX4s~S#vZf1Rb)hl8<
zzVYqb>YC$`SnY>C^}ql34;tbR?6@)d#EHk8d-wFwnt!<My$|i%S(Dmx^DVWDpBb3m
za@Cm+q<ZeYto`olGe1-JqsBdveV_X7lPl-dKXKzzpS!B5Zr>fz<7;Y~o9~att~>VO
z*xFCDeCxJ*?~l%_e<W9Zd8+!;*S+_ZCvQ06HP^iSi|5X5>KnPUZ{{1m^o@^Czk1I#
zdm^#hB9Gkkc<L4B-+aqWd%yXu=s$dP&*OW3dFSo7KXcI=j=S+M{^S>@ruLlp&>dIb
zJL7*=wJ%BSzoc->%{PBHn6-9K>@Q~i)57CVeAVYZ`_Wj(qR;<45>5PJ)!JJ>{hI!r
zZ+`f=?pyyL_162p8mWyZj(K-<{loiuF8KV-@A}-0H-7H@CtVwj)xM`;&lg6Xy!z&U
zx$NSnZ~pA-cD;PgS0exMxiznQS2DGyd1Yef`{O6S<K{h8RWF<SFYPCWQ<(kbuSBBN
zk?Ci>{oc=R`@)=~@6%8J?c$cL%eSxUxMIipj*Y9{I4k~N+NZ5k_USzP)IR-h-lrdX
z*QMKf8}r+SbB$ZhTm8mavH#LOZJDx9+w4>O^uKwZ{`IWQJ9<mY&fC1A<;r~T8)rrT
zOZ&8W%06wePwmtH=6!nW1zoN4a_z%gE*<IFab<aDRqg(GB=~F)1h3xxY<LO)00000
z0000000000000000000000000000000KjjCWr@8fM7NYmgT?ckn)>sj`9j~^?w*0U
z;b}IPOV1t1l$!c8J=t_qw!g2bH(SW`=0}=((>t==`GL9N;iA^&mbS*`g;lRf?2T7?
z3OgDe9zM`R*ke;`bMySB{&Y{KacHn{ym3=oW6Qjj1q<4mo7<XO=gm+2${n;$>0n;`
zSMH!?N(XJRi^2|)ze)$qQ#xp=4;M5R+!G1D9UcGx0000000000000000000000000
z00000000000N^)RLo6Pdzc!cd&J>$A^rs7@#rgcUFx_@qG#**Dc|5bWkRQwxO4+bd
zPo~&i$PSjW`GLByeJuEgNbqmr0RR9100000000000000000000000000000000000
zev_RRUzXgQ&u{Bb7q+b}<Oef_QZ`d;>d6$l3)#U^Ha~D;e13FoF5MkQH-xoHi^prn
z>dWo-yu|Yd00000000000000000000000000000000000004lOSbg|O1a*<%?ZK8{
zes}@^000000000000000000000000000000000000Ko6K*@<b9Xf$$KZ8Tb4JtMY0
z)14V8ZA|wS6Y)q@a_jyFFHDT5)h+Q#)s{-t`tho{os0ANZT;!OwoK1NlZCs+)8O}G
zl_syMG?^Y-l`obq%NDbxgLUV9dp!N_*68#|G#S}DRyp3@)05gf-gPlm%BM<Ovc*(y
zKG%~el(WT5CJbfzQ$5+@U@pBg)04^%lq0Fd2U=~+=X0CWh4Hbz_T=A;rys8muRIa7
zM1uQ*w*^;)CjbBd00000000000000000000000000000000000@M1J35@%h|(4Qae
z+MLgC>rWT96{lpnvZYLacAzISGCvjm*A!2jb<y)xA50fA1Eui>ZK;;?Vu`ahyiotS
zbg|T*@5%ONGd%~#YK<n&T6X;NH0>M8_H+#mWVa7x##^=2g_krf_*^7-CU`RVY4CXP
zNbrN;q2TMmKLz)NVE_OC00000000000000000000000000000000000_zhAOk4CGe
zm5+(4@-dz$A7k-EJQ<B8;*n@w`H(ChYRiY3@}W8&k0k5D*3*LJk>EdrUj#o1z8!o%
z_;~Q1;QF8xbcJC600000000000000000000000000000000000008(Ey)51s?JMNx
zrgORMXuf~$@bK{5X-5jy9VwVRQn2bs!Nie*@goIe$HyC!hc%B!qP68iP5Dq=7tVZI
z@Wn{*Oz>pz)8O&ok>CfxL%}zK2ZDbL!vFvP000000000000000000000000000000
z00000@Eax(Pe!Me4|U~3G7+zh)|L-7<wJG(P*px8%7=LQ5R1oalXYRsX~7+l;FrPA
zgC7Tv20skG6MQ51r{GJ$Jz*FC00000000000000000000000000000000000004gd
zRmWq|$PHCvNn$LCk0r6Hcq|!~#iP-ly2>$`h}TAI%ZHlsp}HJ=ZdzT~&9vZSk>Hu&
z$>68K<G~}r4}yn+Zv^)RcZOjA00000000000000000000000000000000000008(k
zH9cM(jZ`JK?tko-r>7mv#ICJ7ka={~8OcOE60I#CYRZS|@}a7HNR$up@*x(FN0N16
zkMZDvNbu9(2VnvL00000000000000000000000000000000000006%=W+bB3(P@#Y
z<ktO<-STw2lC2xd#;%RkhuLJZl6`d58FgX(c<_@*@J#Szm;e9(000000000000000
z00000000000000000000!0(2tcr+S|C*sNQ7_Uv%g`s%x%}DSc!M}wG0000000000
z00000000000000000000000000002|mYE)}jz+4ITlYV9%hU0Lnb@_lY4Pf0CG+U2
zGwQ;cGlO?Uf?ozd3myx87(5g_7~CIxF}ORpJ@{ns*TDyZo5NxN000000000000000
z00000000000000000000004MtR41azXzh$jQd3_^s;5_ys%e!ZQCCUg$x0Hdtx6=r
zD%FWdG#Z~-Nn$l)N!3`A7)#<~NvtYNlCipQsF}f^MuPtgejYp#JQ92__-63c;7h^X
z!R^5(gTD?w5Zn|N0{{R30000000000000000000000000000000002MOQ9wajYezY
z)5g+RU3DUwjMgN>q&8YpTgk<%E4ldeN}X7|W-N`(tdz!PR7zv@)rn*@8n3D(u|!oO
z8MX|MVNoJcTbry42b&q39SMFG{4jVh_(E`7@ZsR5;BCRapcwQ7ox##zL0Aj`00000
z000000000000000000000000000000000#;64B~tZKNu>b^n7GcFw3|YsRt*@2{_9
ztH-kQTc=mDRb$x`w@<5N6Jy!q-(OeB#>cWV=O-)K*jTpqN452dWOZ~b7x~NT`b1<R
zw`lpyDY@9BTx%gdDfjZ3H3xHFdwgNl!Q6efz9TU`%q1ssb>X#5+jVm!crf^2FdQrn
zj@$jU-8b*vyz5_g-5Z7h00000000000000000000000000000000000004e%oEcvi
z9n7V>Gv}9f4rUgeduB0TC@pH4cV>TjWPPSMlq(e%wKUhB5^o=`Rw-XNwYd7~#NM6b
z<-Pe_Po}VF@tQR&R<*BRv7u{K$Lh{A$D^IAmo8h`v83xj=|sWCHEUKbZeM@q_MuE+
z=f?6(TE=J6I(4WsGb7!(p`OgTuwph7MrsztgXor0X|Q;HQ`7M9@ZA3VXf~Hi&&?P5
zng$E`t(oppv8gv($n@q%n%Wl3ZBDn8v&B+jXrL)MKb}8o>xo7K*}g5M+|Glo>Mn?1
zebiQ&fyT}aM{hf=Dc=1e+iuC^2Ag8@;{7kOe)mwZl<&`uW}4zH@oe%%Hp&c?3h7)^
zW4!AHhUw0SHM5(CN|~m_+40U7sGZ)NA1XCf)rEU(TCg+{JP|w;d?mOe_)zfY!CQiz
z!It3iFbn_y000000000000000000000000000000000000KbA`;<KV7t(RX?+FIOp
z>F}Dx)5?M2^YTkOmapt;T++Cywi0N*xOupBQ+L<O)>T#IK=*>>JsTEoTH87Qvde4A
zfv&~vnavyLl{)gH+v+NT_TeS18%NvQv!h#M<-oEvD>@gh%5Ge@b@6br99YslI)7m0
z=+4f;b(^YZ#%Cojo<BTv`4tPYix;e2SFW)5k~Ld*T)tpk-)N~2F9(`eU9xS-mcA=@
z4i2Rgb>Zx12C+!+MDS4X<>2<<uYx}dt`A0nzTom;X)r%HBRD=R1^@s60000000000
z000000000000000000000002sx85=FS<xLkR?c6YDXkh9SlCum2{c~1VPHjT>!zWd
z%d5+Qp@H6PF4NOhR#gs^^7-86bfFT6R|4t2V%NCD%=oNiA)hZz1j-fqGXp~t`B*v7
zyKZPn$M&VIotv-hm|hN~i|vD3J2InJwytiQQ4VYxY|Hd^cJElQX2FWOO5og<p4Kb-
zmtWl0ao)^wU}K?mVdu)8!R6V^;%Vi;`g1QETypuyrJ2^{OOoZl>XEMbTQ6O^w)L`s
zj{0(-al^XJJF?3Qxh=(FZC!Z@_PoSj0RR91000000000000000000000000000000
z00000yu|9lPa^n!B=}|U)8I$p2><{900000000000000000000000000000000000
d{8p+-M5ECeu|y@UkHstL^jNGa5ltrJ{~s!yL7M;o
new file mode 100644
index 0000000000000000000000000000000000000000..b43dc2389e4a17471bc01ae421f9521fbc2a0afa
GIT binary patch
literal 1179648
zc%1FsdwgA0ohb10%Ht$W%R>yH!YSB7rD>9ev;{;+X<AC(PYdb|r^!ir=t)j`&Pmgh
zw!5t_Q05Naab|SJ5xmzIBBL|vTon|#FoG|J8F55M@rBCBaPcuJDv!I*Nzx{53OJwp
z`QYRG{WN>+_4uu|_S$Rj?EaMv>sDq<nN)AS(4Q`)+9LZSu~_8dR4NjQ#KWJLMh<&P
z_>-8BkA^>|MxI$b()5FxhP@w+oS8c#vhU#DkMH^9-s05{?0MtvJ4b%Hd(X%PBcIsy
z@54{+`ta~ehyVA`zm|qeFDu?!+*o+HFtX!UJGutHHn<`G?R<LRO9NZ_zuo_;+~>k>
z000000000000000000000000000000007{3*nyXxS<|@goalkevjaVuUH$ppUD;Bm
zzu1)?7|0hZ=jl?Zkli{|%1qVbrI)Pl?AX|u+O&H4x=o#_<*S!;Zb?0RFR3-Fk8G7X
zyPWCD_RLL{^IeAm!lF3`W}H#exa{=kfzCsd80~s6m+sCSJ;O@nna7&jF+<0vH8!D2
zi(}0wHnXO2`B~8eGY<{AHJ{(!pDt`KcJ&Qqd%A`OvO9({N7tTltZ5xPetcesTBOb{
zH<`0DRnvIM%;<rW4vkWo^3)R;t(tkP(T@>1KJ;ky*to^hYZ^PkagRT8+@V5l>S0sQ
zGVBq7M~)g+&pB}XX*G=-!WD0yV#UjQCzmdk`tv>6-fX7l=q9I+&0wnao-%rhMep4{
zH&t%8YOLLyy)UV0yyzI4U@%?C43v(pIPDoGG9hM);YVxdrUvuHY$=-`n6u->HH{a9
z!#5mbMUF0h$+N7&GhKOUWH2-5K>S7Fo@<_X$=kBUQob;<GaJsT*wvGs@ElI6dht<X
zA0u*n{7KcRvny$(RXB@NpLrI&g?#_iRWEv$SsaQy#w^P9U6st71C6KDG_DF~anUhm
zQQ5x7UXW9tWh#@yk1?OaFR2OZv+v~abvbk5@{B!a<=tCpH)&R*m8Tpvonr<a`FxgJ
zbWIq)zp<wA!f^cLk>mFkGToVh?vW$PPd?VrliD9S>Uiaxt6o^sxNxfBDpTst4-J$K
z7d9Sitiz3u9BA~)DphkbC)G5zhhs&L9BW&?SURHYg~uA|P}?I%Dr9z6D(CdSAbhmL
zVd{@s!Sv2-cYffA;**ZKfD@V@wSJ>@<zHtjPONU6H7ok6QhIAHQ{0gY-vwR8Qo7VS
z@%#m2gKX?rys|SjvF7a3*4*4|&#t-U+?<22KB1=ZoO7bD%U8NP;url9*%K#qdelcj
zDwXXCuWDoGvd;CXwd<F!>R5kyYDMSeb5moV3x_IKui2Pdy=mpjDSlgzx0##j&-Q1!
z$}h6i=8pB3T-vez?6&!HQcF6Qc5GTX9v&`9DSXeSR<2pSY^>GXRBtZbS3ES<ctFeC
zRA$#;wvaBrj;E@v3^hf0`6=n>>B;m=RXOoj%+d984z4)9rt$RCqp!;xTHZqoTRA!5
zs3m>Y#T<W&{PC^kf#YV>G`6)x4=kIsnuo8rI7QKMQ?BrY2G6wC1L^+B3pVyqG`{3>
zI*+Svyu2+MsXVk}k6C)Cl&|EwrWmcOb&A5{4>UK_lppGYGY?(a*b`rzcs~88YZ<S3
zri%!_$R^&>v1_Q@igar6$~B9Ru{$Q+;&KObQ{_*})W*&&8|N&XUfsB2)-zrB_|!(P
zcsw^_?X>Fh3+M7ludht_H963IWVT_-m)Kb4+2wmPH_Xl1Q&(BL0~Z`xyX~2g;e4T|
zIPtvxsI?ofIriG6IyP-wvwU?JxvF#Z#<{7nh{_6#E#sVp_0P5t<Kv7j#CUGnftuRU
z-Q0d?-iJT%3i*7gI7LzNsH-@lLHT0JCtZ`aZtH?Mhi-dsKG%~eOn5oW=}bP`CO%@2
z(JMKku<qaqH8qWAofUm;_t6(LDN}pwi+HAeJn@whUdHyL-Vmd2vC&WO!wttj!4H>|
zvZY*R@>_1~u1tDmjxAJW*zz5p;^j5<D{JC&a$s&%<u|~=vkomv`J<tD=%o6n#i$fN
z(?XORjPAkl_t4>+JNBJ5;bOz-AN{*x;#+sHkm=3t8l7%yb#-Is>CdzkDzh8i0F~34
z0}J9cjZIC_gQp+<gcukcDvf>o9+^)Z{Sh&twtU&o@C&ZebkfW0aNSTbGy2_k_}V8v
z$`ii6E4|E_b5WwYaYNHHUF*b|jb8G^lB!4~k~pv|R@2zr96fm9;rl!BtN*Bi_|f-!
z^8XT!ecuoNe`dn&K4P&Z{@>!rvdIrnF;gr~cr4}|ygFLbICEz7^&N+<Wc2Iv@LBAr
zxsQLJrk?Y!pD7=|@o(wLr4zp8r}|phmJJ`pDLy_X?1{;nf2tOTKddJ__(wcMhXR{g
zCqy3cAE`r)Cp`V-cSs?V?Hf4OMI8Q+Xb3k`{nZaeg7*Zg_C2=mZTpt)JAUuJy&Ze*
z-;)oE0RR91000000000000000000000000000000007_*&VkzchRD^+o07G)(P;Io
z+HIxMVDbFs=HcPtdHwm_*<3C?FJI_u&I~kd+R)saEo6H0yP5~GecMX8k#fFRDhv%Y
zFCDE&tsbpNEiG51)(;J&)~5S1eTDqIbS{_Oo$sGl?zrmdNL8}$rYE+2;EI+VJJzng
zw7=N9xTU+UA(D?YC6l$0XPU_|=WygGJ)?sp?rmGJB75oL-3vGDYVF^iY={KUKFDAp
ze^sWtR6I0V+roJ*>DFf(>^C>xvTkwqs?A$=^=@B#Y10L@4Uu(`NNqS+ZDdxm@`vfq
zFaQ1b3m42T<_o0@ThE={pWd}TQyj{biWj!F6m~A@TsH8^-779#*mmH!n(2`PBS(h!
z=5sxn!i9_1tXZ+DWBrN^U8_1*Z<;+;x@q;&<tsavbRDW4uh_U|&C10c>u2v6$`nR6
zmS@m9HiP+-XD~ant2;N;lUWxw%x1!p&FgXtI@)txP2Jg*ZPg8tjw2^j>1NU7Ub5}!
z)hjQX-<Q9<ziDk%Lu6B=sW#kTu`uYFH&}XWeyDWR;wB>3wQnD3U6fruxTAl=(56H~
zr0co%*qt9J6|!50N|~d&jYqEAJ<{DVxU7H4s{SP%uZ}lF`k!mJ<NKm}s94JPXLldn
zdn|HY$J)V-UGsPE=-t+y-5hI(bXR&$KKI_YWpaZ@bsV`a-L-Z>=eof)T^${lT^?<S
zWGnqfev4UW!bM5vrs#U8qqK0_d0m$cHeH%+2;UF!;O<E9e}jJu6951J0000000000
z0000000000000000000000000_<eRlye1NjR!3`N^^vM%-%U?!`#{z4v6@J-Tvj()
zl(@I*xM)qJHd>vmO;(D2bMq|?<>>v-^Xvft000000000000000000000000000000
z000000N{C6S3c~2o+kkS00000000000000000000000000000000000007UkhVUmE
zJQ4~20RR910000000000000000000000000000000002MAA=L4)sZt}wUPe(?rbiX
zo|iB5MXsw0E26<qBH=#(00000000000000000000000000000000000008*oF+F}}
zvOm8&o6Du=<qLfcVbP2|ACClI3EmN08MN&C>AsKe+q3T#`=WdA+52aEU$yted%ho5
z0{{R30000000000000000000000000000000002L!_Jv8EfS4Hqv2_F^{jX-7O6`1
z-So)ik@2)^=k6_Qmld*0bBogL)2Bs}(P;9_O5@tt`b>9bpp@E}?kkQq-*obL*x-_u
zog1$j99*!u&~#x#C9Jls5>_)7mK#Ye&gZxHrwiLNJ)>dkTE@HSTDoCbI^R~<(Z6xS
zf@!1Svnt`$@m2X^DYZFU%$AOfTi3E=Wq$GMeEZ;4JJ&C3tFOdGUs8#ys)=`O%@37Q
zo5tqa{=jH@-e-od$aQWg?H<^&WMgJw-DuG0V2PU8vP>zpd|+ojw{v{7)*p{|uyA3z
zZ)<Mls?zdw>jlZtwP_w*$eK%riluyic6TPVG+W5@=68(-&8r>nq<=@t;PTwi&?OtT
zr(a$>dWo%-;8@M2ncQG1J<yZdI8@3PvgzE|oM$f@O<$I3>s)hQX3L^=%ezL}&aWB0
zhv$q&$HEn={9XFVBag?nKajqvZP8F^XXoba?LCXC!;zEG<Y;U>yxLNBpf9zdlr9`v
z-qW^?rf00p6!XKYO2sR-Eh=t#c~yDTWOQuK@l6B8d^zUOT^*hC58Cb<jh=qb&_HiC
zm+9#$o0k}kK51;uhxX8T`!FqqWnE*=k5}4{EpP157^#i<d~R#HFnO$XEv3=oO6<Z|
zCAN0Xk+G{X14EMoFOT$xW#fa+iIxLvYa2!bkDOm2pD&F!4Vy%&g1aNZ&x1#T2ZQ^=
zQvd(}00000000000000000000000000000000000_<dDh6@O85XrMQn%k)&$CgLYW
zOX<F1qCOrLm-6}C)^s6W8w-p3GXq1hWHf$Caxj<f&J^py;Huz@k>HoXqrpSL1K}wE
z00000000000000000000000000000000000008`6tB=1ZIyBIm&1HJ3%IBqgKDRYp
zh}XtXik8xS#Y8fGN^&rl?#>j;Mg5t9p;$vXaR2i>aR2}S00000000000000000000
z000000000000000c%Idj5Bs0zNdN!<00000000000000000000000000000000000
z!1Ju3eAxdyPXYh{000000000000000000000000000000000000G?+J;ZN0$Pet}_
z+5O>NwL@2irvLx|0000000000000000000000000000000Dm~%n5s%f4xDt|uG6b$
zZ7Y=qi|02t_vd%#3w`stdj{r(r`cRCJ#QdWYVObUWYf*r{=VkkY$4N|-__ik-kI&r
z56lY>7tL>JZEI>-bnV*J%NpuVtqZ$uSbph}^<jEk`KPuzJf8lur_+&0?8)UzIyNr)
zlUu&LcjXs%ytwAafBJ(t=RdM})tceg4DSuTpPlxJxBc^LFFo%y*&qG=`Kw;@-pnoU
ze)-2vo_%~~`-LC;;@qbHxpB*@&b@W}yzZMeytOd=nm4}X+!K3${i!GK?7#e}pFQ>T
zlVAVgpZ@ZV4+m%6G_>}u_rGHOUp(;Cz>+5i-}=h!cRzkt&0}Bus|Cd!+itn{$s0OX
zeEOB8o?kur(6<ZUOq?@PJMt&fJBGiN`rxDS*cBhF{b1XTpZv&U8&*B~k9RIPxBIeR
zUAy?(FMt1AR-Sf2X>fk~MJImj&+C?)blU6g{@5#aEqvQ|)-J4FckO9cZ!fi+`l^2(
zIp<dozTn1=fB*Ea-t(qA{>L46?EXJB?>!;;&?ox7v+n+DF5h(Fhd=c28@fAc{^Inj
z?t1+F({E^c{fDpm;hfz51$TU8*P_>_cU@Go{j{1d&zbYzdJ^AVzwgv{)O~dDbK770
z$bkz6SA614Z+h=vf9By+rd{+uf41Pph0C|L-*n#FzP#wVhX!|D-SLJ$n{(OVDMLU0
zV(txJc<_<q-v9a5g+G1qz(+S8czVm5KHdBFuT;PM%_lv%=TmRK<K5r6@|y2m-}n#J
zU)b8#deNt2Kiiaf+bw6`^^W`YeDN*s``+D;Z~Affvin~BiFclL#sz1b@h|7x^=I+c
zX-nJRc<+^6_rE{(;g*($cmCVoe|Xk;RjdE{?`Lj#d)<e=^0GZwpK|bq-s%6e{hPCM
zjptS`|764Q(_Z^`(>}Cu+Iu%n|4`eLSAPEcgZV#ge!=I@u6^>m*Z<{5rgvTNjn;P!
zFZt?@ufFg_gCl?P(q*?Fc-NXcTHd|&<jbyn|AQY*w0`=j<5t}Hoin~U{ifmLc7N!e
zmd%eHc<Rd!e*EuVv-AGym)0d#4{zS}so=JsEIi@vE5Grjcg@_L{KYLlUzUFSWm~2f
zPj0&Yj(2?NzIXrl2dC$TzOg7%-}#E~{>?3Ke(7!d;;)@~Fr9is`s{b!@&4YE|Fz}1
zcelOmf1EPyvg5yX+8O6B{N`UZZhPJT`N&uIzWZArE4<--GpD_we_Hbyr~G=j<v-6p
z<wxJ&`id{TX6^DNUw-PYm1kTrch;@nnA?~BYUbL9xBuA<Z@c|d8}E8$(=Q$`yx|km
z&VK#$_bmD7UtIGa?~6Zm*E_FSvgD(0y5``uXTSBD*fp0fT@w5CBX8LkJ^43pzVX`!
zy8fa3^eyjPz2t8fUwJUwpYF>v4-WK2s*?Y?``eGcad&6W^3E06((q+hEPef~MEHot
zuYJMTBN{%T!^6Xe9?kGkZJyuK(%#&k?#VO_4K|HEmCbEUt>?BbT-e&aaDGesdFMTc
zCo_`h>sZwhiG1jug;j6*_b`7-sdGzdL%z2(oGxS{9X<K2nbh*~T-RsPJtI3lmw9<4
z68+cm<GkzZTc3FP>5JmCBB}jvyZ*e#zIA)wWB>6VPu%js4?gj&KYz<lZol^0&jqQc
zzxt!E-2A`4b?fk&7yflL61^)@h}Pb4Uh%VEdf<V_Z?BKv`E=XGx7A(qSZv3s$K87I
zPd>2e=AC!ke`m|ZkptJhy{+#R)u;aAq8EJN_$7ZC|4GnY+wk3U=FEArF>&p?PKg|M
z@wEF|9-q+`x##iQU-VF2_ciJ7U)gxd(=!?xs$LY^`L%D}TGtYf#A-kIiU0M#zSkJP
zf9Lhl$4)+O!JX4~*ZkwH?|SgyNKI=0jqj~p{KUYt)+=Y<pX#}DbH|<4vp-$;qo)0l
zgP-{JFIS%1@YwZ_fA-4ex`UsJo>)`U(sExkcFpk*#nyhb^&7X|eP8t4hKF<2uS`{c
z@|t(O_|y$2UA*UopI<Pqxo_9)eKTJBg|B^N+SU8_?2p85jXZqAqp262f8%>^IPmpv
zME~)_`ybu^>)UU;?TL$Cd&2d9^B2E3J+=Sj2S0W7-P8YjRmYOlQ_BkPz469x2ea1h
zkNwSze_3?m$!C7{Gart1UU<*XBhkd4R;_*iCok?FdE-MTbie;kQg6QR%aPi6;<$H2
z*FSWy=Yo4~eEVmwzy7oDIpuZHSnWF-_kV8JFR#Aw-!@<Js~bP_>b)<VdvD}FKfC7D
zZ%?N7x2#NzyeEF@pWnE@s_F&v{;lKWa0+w2bZ;bD9hr94TkrnN_Rq~d_CCG$Yj4bt
z%wMpnebsqu&+Q#~{jB(ZX`jxYv`?SQZxH*`KK*U>=~sTZbU{mJY2*C1OP97TfBmf3
ze`%k#PTHpn?Nj^o|MNb*@3UR6T)ed9imtUgOP5^r`dQKc(mrjOv`-h<r}pXp=Y9Hx
z1)YVhL)$ZJF5j5h)?MCNRr@{?2|g19!7KJX8J+?F0000000000000000000000000
z00000000000Py=^dE&rH(QT#DVDbFs=KlQde4%e%chA7Q@HCstrRNP~O3nS5o@}}~
z+uzsRn=NE|^ShdR(>t@>`GI-i;iCC1t!+&$i>fY89EewX3OgDe9zN7V*kkkjmX`MB
z{&Y{KX=t!%EV#L?srB5}g$rBT7tU{KKkvN6Z{5NCNgXVT|JEI}PU>J`?4q!P<ZshK
z%cKq#G=vKp3+{>p-wY1`00000000000000000000000000000000000008iNtT7gk
zw6D#jyEDb+4gKjtX>mTkJxsUHjK(9&w~l4j7V?9cLMa<I>d6$l3)#U^Ha}1o#>awx
zj08Un4*&oF00000000000000000000000000000000000@cV3Le0g$fKEJ&`UD&?1
zkRQwxO4&@YxhGTXE@THw+5EuC@%HH2T)I0f-4M1aEgowfYbeL>f1c+L0000000000
z00000000000000000000000000002bvxe{|5!6M3w+7pS_V5G%000000000000000
z000000000000000000000DwPoa}xEDXf!gjHX5z2o*rAD>COz4Hm3WEiFl+c*>}?;
zTRuLPezG-QY1&$8+A!8MH?laN-`<}tY|r$JhipD`ES>eTSS94lO31X>s(i7uIa|z@
z4!7O#_ObM@uZm8KM3a#NqmAPoJw2(dV_g?hrF^QiEn7_W=5sxnLOEN^WWu6Mf2t>2
z9L%LhGCisMK)EEf_)yfwd_K1|T^JwhCCRb0z9GEw6ZbSnf=>r;3RVZF@B7KV&+L2a
zzU})i*?02Z@9q8Dy@kC?_RiSz<vs7(vm<N<000000000000000000000000000000
z000000010z@rjAH%TMgj@9sME7s<N{`FyEZ+>y(cGF|DRQa(G-liAfZMVqeH_EhV<
z6B1`#aLT0G$xXv<`YT<8t>>raw;Z21>!N3BKbS6L21?@*ZK>Atk4v1j;n_yWrHiHh
zd{4GFo9US_THB1oS<6p)hS<KLY){wFKz7GaW<0EQ?)1dzos~t|mM<P-DJnHxt*zno
z8WPQ`8Y^YlVkuu3*;)QO@WrDFx(0LU?o3a*G&)dtVX4-(X^G}bPJ9lry@h=LcvxHg
za}1g|)||S;%nK?DQJF^Z@L5-HdS|vfKQMavt*QC5lZlz_Q#UPN^?17lXVoTVE}Xhu
zWdhy#p@CAl;mc|gGcP>G*o91YW}tgyywkSk>cq@tQ@1VO;!4MduB#j{w<<BSW9opR
zLT=Kn4HvvAk(hbO)UA)&3FUhmPn@%^a&OCftk{(v7|0io$X3qNrBWfgb!co}=a%p7
z{8(bvipp&oTi~fT(s*U%VY)av{y3Jq?mE=#)S*YT>1v&yYOM<&>iXcbk>H8om%&ei
zM}voh?*$JA4+Q@bd@(Er00000000000000000000000000000000000004mBBUSNe
zw5q;*OjMPR@kIF;iznjAXe<$rMC;0jWcg5AKGc*C)$w>FSr<mv2bV>H-vqx1eiD2$
zxF`5X@Xp}cpcHh4#Q*>R00000000000000000000000000000000000@LPI8yeZmO
z$j?jXa@pPa{&~a0!}IE=Dy*BTFgaCW)l`LvsS4v$6~<1CHzkh>k4K`l<wH&RP+b?!
zygvARBzPkDW$@GB(ct0Wd%=Uj*Mj?le+r8M000000000000000000000000000000
z00000007|kOd_6))|U@;<wG(NuZ`B04>jdOb@@<LJ|xPAc=-^E$7_>yVPt*qsYvka
z;OD`QgGYkz2j2?57W_-_h2XBR7ytkO00000000000000000000000000000000000
ze)m<!W6{WURijB_G>MNUv8s418P>(4(Vn`>F`0<hMr+H5n)0E#T=;Z-UD!>1@b{76
ziQt#PPlHE;hlB404+dWgz8Ksd76SkP00000000000000000000000000000000000
z!0)JO@#<)#D%p3_6Wcyee>ju4x9(8pH#gssOvEG6+VY{Me5fuTs>+8%`4BH3V)1w+
zSr_&g5AKfyKMlSYCIA2c00000000000000000000000000000000000@CRdhB3d1-
zk5nc5ZhB(d2jZ1%-Doy(Z>%BACX<!yZ*IP&E^HqUei8|u2!0tR000000000000000
z000000000000000000000002+$Dt}7jmF}McrrZ3Ym;?hQ9Ss1B>2zZhhYK$00000
z000000000000000000000000000000004hrrp2qHk*Z|hO;2q5K>Tnfac`_XUY)FD
zesl9Jbz#dH!P_IjuY;cj{}FsYcrf^Ca9{BG;LhN-;A6qx2JZ`Q466YE0000000000
z00000000000000000000000000O0vioror*wbLs}O+zKAo>ob!>MKd2u9C!)l_XYM
zl}Lt7suPiDG(Mw}#A-&9s?j7dn#4zwSXG!LV|C$BGlIX21iuM>9y}I29DFDEdhq4o
z3&EYiZNbNazYX3O+z?g+00000000000000000000000000000000000006-Ap(YWH
zMr-2rqiL+JIuT7qYm#A78?C9W<l@zpTzp!kO)OqBn#N{SYGczYwXufkL^2wUS5=Z&
zqAHOLBg11@l}OaqChNk%W&~$Pf}aK7555|FF1R(gCAcAYOK>nK20g*1U}>;0tOfu8
z00000000000000000000000000000000000fQspfXmzwUQkCqx>5<DL(<|AU(d?#^
z8!FlA(d@dGX_ai%X!g9%)K{{J(QNwzb(L&<G~4>)WF;FL&Au#E+mJ|BM@MsKtgUWH
zM8<QMM`ldQ#U|v|wZtdn+8?Nykehx_)#2O^+U`qC3v<cwTwQo=^?PrO1YZqq3WkHl
z!3q2Rb>EHqw(kAG-n+wM00000000000000000000000000000000000004mB8MEW-
zqJz0~cjo-k$YAEe1+$C!Lg~WRb7%LbcdgG9hjOLjg{>{Mr^P$QnpNr-O|GuKI&olR
ztiCs&>&X-@T)bw@id7x!S8V87)wz1p?6J~KtCucc*}0_aQ0;id#x-kJF78-Ad&f|w
zFtV{clh(1B%%42e?98t2+)z(uUDz<22}^1&j0e$erP5&W{O0E2;o*7x`Q6!EE<G<_
z=xZJ<<gdzfmx|53*+Ql_zpJ@z;k=e~YdKpi6@~_ylkM^RF{8(W2C{wIO1Y84QFRx@
zuRdl}W}s=)hGWOpH^;l5W9+s}Zm>CaZoL0Fw(lM)mh%1C-I?ZiYdo8LjzO7$QX!pd
zZi;t3%P`&fuw{1ZP$|=#I6J=SSz4#J=7&nnRdwMWs}Gh&g2#dfgD(ZQ1vdwO6<iza
z3i^UqhQ$B?000000000000000000000000000000000000PtHlE<P)|d!)N#a9RJ7
zRsBmk5;Nknl0zM(h1<^Sx@@rN(rmdwVds+0WdpC=z2ee^ZMEfsY<qh3%FE{W<uC7V
zsxB9#yVfq~TsOF;tE1zxSh?VemK{6RuD-Ot*t@u;zFe?*U2Z`~d#<agJG-)`T(E9&
z_NvWWcJ*#wduda$T+p$0aAVi}-8*`>wP)kyg7)nrt&6hD2Y2*u7^*54v@KYXy>#*J
zg&TIY_Se;gv!4;fBEe(9gTa@A+k%^ezY4Amb_IRG=3r^i9=tR-F{}mv0000000000
z00000000000000000000000000N@Yaaq(Hv;VW{T8%nzewk+A0sV^4{4fJMnnVzn)
zs&YXopU-Vg7b*quN<q4>*fl0GBR(rx$mdJr1?2{VOImhrylQZ8!RA8K^m0M}j+VjY
zxuKy;Hf&EPD+QT>q49`Vxgb-_53ec}uh_PzxTU&WkiM#I(NJk;=jQG0JvHTmuARHL
ztX)>fF3l}U&!`kE-LNd3Z!7HR-?*WnT(D$ie(~yj``}eO*Dsq^E?BrQ-M2Nja#d-0
zy0xxc(AK%;yv&wG>y~$owAI#?mtg<%d<y^o000000000000000000000000000000
z000000N{C67ycxI??!@O2R{vd6rKP800000000000000000000000000000000000
f0KgxlnnW}jogPb6(uP>Pl1_`osuIy;GXDPnRw!H!
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/migration/test_current_from_v25.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* setup() {
+  yield setupPlacesDatabase("places_v25.sqlite");
+});
+
+add_task(function* database_is_valid() {
+  Assert.equal(PlacesUtils.history.databaseStatus,
+               PlacesUtils.history.DATABASE_STATUS_UPGRADED);
+
+  let db = yield PlacesUtils.promiseDBConnection();
+  Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION);
+});
+
+add_task(function* test_dates_rounded() {
+  let root = yield PlacesUtils.promiseBookmarksTree();
+  function ensureDates(node) {
+    // When/if promiseBookmarksTree returns these as Date objects, switch this
+    // test to use getItemDateAdded and getItemLastModified.  And when these
+    // methods are removed, this test can be eliminated altogether.
+    Assert.strictEqual(typeof(node.dateAdded), "number");
+    Assert.strictEqual(typeof(node.lastModified), "number");
+    Assert.strictEqual(node.dateAdded % 1000, 0);
+    Assert.strictEqual(node.lastModified % 1000, 0);
+    if ("children" in node)
+      node.children.forEach(ensureDates);
+  }
+  ensureDates(root);
+});
--- a/toolkit/components/places/tests/migration/xpcshell.ini
+++ b/toolkit/components/places/tests/migration/xpcshell.ini
@@ -9,14 +9,16 @@ support-files =
   places_v16.sqlite
   places_v17.sqlite
   places_v19.sqlite
   places_v21.sqlite
   places_v22.sqlite
   places_v23.sqlite
   places_v24.sqlite
   places_v25.sqlite
+  places_v26.sqlite
 
 [test_current_from_downgraded.js]
 [test_current_from_v6.js]
 [test_current_from_v16.js]
 [test_current_from_v19.js]
 [test_current_from_v24.js]
+[test_current_from_v25.js]
--- a/toolkit/components/places/tests/queries/test_sorting.js
+++ b/toolkit/components/places/tests/queries/test_sorting.js
@@ -275,34 +275,34 @@ tests.push({
         parentFolder: PlacesUtils.bookmarks.toolbarFolder,
         index: 2,
         title: "z",
         isInQuery: true },
 
       // if URIs are equal, should fall back to date
       { isBookmark: true,
         isDetails: true,
-        lastVisit: timeInMicroseconds + 1,
+        lastVisit: timeInMicroseconds + 1000,
         uri: "http://example.com/c",
         parentFolder: PlacesUtils.bookmarks.toolbarFolder,
         index: 3,
         title: "x",
         isInQuery: true },
 
       // if no URI (e.g., node is a folder), should fall back to title
       { isFolder: true,
         parentFolder: PlacesUtils.bookmarks.toolbarFolder,
         index: 4,
         title: "a",
         isInQuery: true },
 
       // if URIs and dates are equal, should fall back to bookmark index
       { isBookmark: true,
         isDetails: true,
-        lastVisit: timeInMicroseconds + 1,
+        lastVisit: timeInMicroseconds + 1000,
         uri: "http://example.com/c",
         parentFolder: PlacesUtils.bookmarks.toolbarFolder,
         index: 5,
         title: "x",
         isInQuery: true },
 
       // if no URI and titles are equal, should fall back to bookmark index
       { isFolder: true,
@@ -382,26 +382,26 @@ tests.push({
         title: "y1",
         parentFolder: PlacesUtils.bookmarks.toolbarFolder,
         index: 2,
         isInQuery: true },
 
       // if visitCounts are equal, should fall back to date
       { isBookmark: true,
         uri: "http://example.com/b2",
-        lastVisit: timeInMicroseconds + 1,
+        lastVisit: timeInMicroseconds + 1000,
         title: "y2a",
         parentFolder: PlacesUtils.bookmarks.toolbarFolder,
         index: 3,
         isInQuery: true },
 
       // if visitCounts and dates are equal, should fall back to bookmark index
       { isBookmark: true,
         uri: "http://example.com/b2",
-        lastVisit: timeInMicroseconds + 1,
+        lastVisit: timeInMicroseconds + 1000,
         title: "y2b",
         parentFolder: PlacesUtils.bookmarks.toolbarFolder,
         index: 4,
         isInQuery: true },
     ];
 
     this._sortedData = [
       this._unsortedData[0],
@@ -413,18 +413,18 @@ tests.push({
 
     // This function in head_queries.js creates our database with the above data
     yield task_populateDB(this._unsortedData);
     // add visits to increase visit count
     yield promiseAddVisits([
       { uri: uri("http://example.com/a"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds },
       { uri: uri("http://example.com/b1"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds },
       { uri: uri("http://example.com/b1"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds },
-      { uri: uri("http://example.com/b2"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds + 1 },
-      { uri: uri("http://example.com/b2"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds + 1 },
+      { uri: uri("http://example.com/b2"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds + 1000 },
+      { uri: uri("http://example.com/b2"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds + 1000 },
       { uri: uri("http://example.com/c"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds },
       { uri: uri("http://example.com/c"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds },
       { uri: uri("http://example.com/c"), transition: TRANSITION_TYPED, visitDate: timeInMicroseconds },
     ]);
   },
 
   check: function() {
     // Query
@@ -560,51 +560,51 @@ tests.push({
 
     var timeInMicroseconds = Date.now() * 1000;
     this._unsortedData = [
       { isBookmark: true,
         uri: "http://example.com/b1",
         parentFolder: PlacesUtils.bookmarks.toolbarFolder,
         index: 0,
         title: "y1",
-        dateAdded: timeInMicroseconds -1,
+        dateAdded: timeInMicroseconds - 1000,
         isInQuery: true },
 
       { isBookmark: true,
         uri: "http://example.com/a",
         parentFolder: PlacesUtils.bookmarks.toolbarFolder,
         index: 1,
         title: "z",
-        dateAdded: timeInMicroseconds - 2,
+        dateAdded: timeInMicroseconds - 2000,
         isInQuery: true },
 
       { isBookmark: true,
         uri: "http://example.com/c",
         parentFolder: PlacesUtils.bookmarks.toolbarFolder,
         index: 2,
         title: "x",
         dateAdded: timeInMicroseconds,
         isInQuery: true },
 
       // if dateAddeds are equal, should fall back to title
       { isBookmark: true,
         uri: "http://example.com/b2",
         parentFolder: PlacesUtils.bookmarks.toolbarFolder,
         index: 3,
         title: "y2",
-        dateAdded: timeInMicroseconds - 1,
+        dateAdded: timeInMicroseconds - 1000,
         isInQuery: true },
 
       // if dateAddeds and titles are equal, should fall back to bookmark index
       { isBookmark: true,
         uri: "http://example.com/b3",
         parentFolder: PlacesUtils.bookmarks.toolbarFolder,
         index: 4,
         title: "y3",
-        dateAdded: timeInMicroseconds - 1,
+        dateAdded: timeInMicroseconds - 1000,
         isInQuery: true },
     ];
 
     this._sortedData = [
       this._unsortedData[1],
       this._unsortedData[0],
       this._unsortedData[3],
       this._unsortedData[4],
@@ -650,52 +650,52 @@ tests.push({
 
     var timeInMicroseconds = Date.now() * 1000;
     this._unsortedData = [
       { isBookmark: true,
         uri: "http://example.com/b1",
         parentFolder: PlacesUtils.bookmarks.toolbarFolder,
         index: 0,
         title: "y1",
-        lastModified: timeInMicroseconds -1,
+        lastModified: timeInMicroseconds - 1000,
         isInQuery: true },
 
       { isBookmark: true,
         uri: "http://example.com/a",
         parentFolder: PlacesUtils.bookmarks.toolbarFolder,
         index: 1,
         title: "z",
-        lastModified: timeInMicroseconds - 2,
+        lastModified: timeInMicroseconds - 2000,
         isInQuery: true },
 
       { isBookmark: true,
         uri: "http://example.com/c",
         parentFolder: PlacesUtils.bookmarks.toolbarFolder,
         index: 2,
         title: "x",
         lastModified: timeInMicroseconds,
         isInQuery: true },
 
       // if lastModifieds are equal, should fall back to title
       { isBookmark: true,
         uri: "http://example.com/b2",
         parentFolder: PlacesUtils.bookmarks.toolbarFolder,
         index: 3,
         title: "y2",
-        lastModified: timeInMicroseconds - 1,
+        lastModified: timeInMicroseconds - 1000,
         isInQuery: true },
 
       // if lastModifieds and titles are equal, should fall back to bookmark
       // index
       { isBookmark: true,
         uri: "http://example.com/b3",
         parentFolder: PlacesUtils.bookmarks.toolbarFolder,
         index: 4,
         title: "y3",
-        lastModified: timeInMicroseconds - 1,
+        lastModified: timeInMicroseconds - 1000,
         isInQuery: true },
     ];
 
     this._sortedData = [
       this._unsortedData[1],
       this._unsortedData[0],
       this._unsortedData[3],
       this._unsortedData[4],
@@ -1269,9 +1269,9 @@ add_task(function test_sorting()
     yield promiseAsyncUpdates();
     test.check();
     // sorting reversed, usually SORT_BY have ASC and DESC
     test.check_reverse();
     // Execute cleanup tasks
     remove_all_bookmarks();
     yield promiseClearHistory();
   }
-});
\ No newline at end of file
+});
--- a/toolkit/components/places/tests/unit/test_398914.js
+++ b/toolkit/components/places/tests/unit/test_398914.js
@@ -92,17 +92,17 @@ function run_test() {
   // but could be equal if the test runs faster than our PRNow()
   // granularity
   do_check_true(bm1lm >= bm2lm);
 
   // we need to ensure that bm1 last modified date is greater
   // that the modified date of bm2, otherwise in case of a "tie"
   // bm2 will win, as it has a bigger item id
   if (bm1lm == bm2lm) 
-    bmsvc.setItemLastModified(bm1, bm2lm + 1);
+    bmsvc.setItemLastModified(bm1, bm2lm + 1000);
 
   [url, postdata] = PlacesUtils.getURLAndPostDataForKeyword("foo");
   do_check_eq(testURI.spec, url);
   do_check_eq(postdata, "pdata1");
 
   // cleanup
   bmsvc.removeItem(bm1);
   bmsvc.removeItem(bm2);
--- a/toolkit/components/places/tests/unit/test_419731.js
+++ b/toolkit/components/places/tests/unit/test_419731.js
@@ -33,17 +33,17 @@ function run_test() {
   let tagItemId = tagNode.itemId;
   tagRoot.containerOpen = false;
 
   // change bookmark 1 title
   PlacesUtils.bookmarks.setItemTitle(bookmark1id, "new title 1");
 
   // Workaround timers resolution and time skews.
   let bookmark2LastMod = PlacesUtils.bookmarks.getItemLastModified(bookmark2id);
-  PlacesUtils.bookmarks.setItemLastModified(bookmark1id, bookmark2LastMod + 1);
+  PlacesUtils.bookmarks.setItemLastModified(bookmark1id, bookmark2LastMod + 1000);
 
   // Query the tag.
   options = PlacesUtils.history.getNewQueryOptions();
   options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
   options.resultType = options.RESULTS_AS_TAG_QUERY;
 
   query = PlacesUtils.history.getNewQuery();
   result = PlacesUtils.history.executeQuery(query, options);
@@ -71,17 +71,17 @@ function run_test() {
   theTag.containerOpen = false;
   root.containerOpen = false;
 
   // Change bookmark 2 title.
   PlacesUtils.bookmarks.setItemTitle(bookmark2id, "new title 2");
 
   // Workaround timers resolution and time skews.
   let bookmark1LastMod = PlacesUtils.bookmarks.getItemLastModified(bookmark1id);
-  PlacesUtils.bookmarks.setItemLastModified(bookmark2id, bookmark1LastMod + 1);
+  PlacesUtils.bookmarks.setItemLastModified(bookmark2id, bookmark1LastMod + 1000);
 
   // Check that tag container contains new title
   options = PlacesUtils.history.getNewQueryOptions();
   options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
   options.resultType = options.RESULTS_AS_TAG_CONTENTS;
 
   query = PlacesUtils.history.getNewQuery();
   query.setFolders([tagItemId], 1);
--- a/toolkit/components/places/tests/unit/test_lastModified.js
+++ b/toolkit/components/places/tests/unit/test_lastModified.js
@@ -18,17 +18,17 @@ function run_test() {
                                  "itemTitle");
   var dateAdded = bs.getItemDateAdded(itemId);
   do_check_eq(dateAdded, bs.getItemLastModified(itemId));
 
   // Change lastModified, then change dateAdded.  LastModified should be set
   // to the new dateAdded.
   // This could randomly fail on virtual machines due to timing issues, so
   // we manually increase the time value.  See bug 500640 for details.
-  bs.setItemLastModified(itemId, dateAdded + 1);
-  do_check_true(bs.getItemLastModified(itemId) === dateAdded + 1);
+  bs.setItemLastModified(itemId, dateAdded + 1000);
+  do_check_true(bs.getItemLastModified(itemId) === dateAdded + 1000);
   do_check_true(bs.getItemDateAdded(itemId) < bs.getItemLastModified(itemId));
-  bs.setItemDateAdded(itemId, dateAdded + 2);
-  do_check_true(bs.getItemDateAdded(itemId) === dateAdded + 2);
+  bs.setItemDateAdded(itemId, dateAdded + 2000);
+  do_check_true(bs.getItemDateAdded(itemId) === dateAdded + 2000);
   do_check_eq(bs.getItemDateAdded(itemId), bs.getItemLastModified(itemId));
 
   bs.removeItem(itemId);
 }
--- a/toolkit/components/places/tests/unit/test_placesTxn.js
+++ b/toolkit/components/places/tests/unit/test_placesTxn.js
@@ -612,17 +612,17 @@ add_test(function test_generic_item_anno
 });
 
 add_test(function test_editing_item_date_added() {
   let testURI = NetUtil.newURI("http://test_editing_item_date_added.com");
   let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX,
                                        "Test editing item date added");
 
   let oldAdded = bmsvc.getItemDateAdded(testBkmId);
-  let newAdded = Date.now() + 1000;
+  let newAdded = Date.now() * 1000 + 1000;
   let txn = new PlacesEditItemDateAddedTransaction(testBkmId, newAdded);
 
   txn.doTransaction();
   do_check_eq(newAdded, bmsvc.getItemDateAdded(testBkmId));
 
   txn.undoTransaction();
   do_check_eq(oldAdded, bmsvc.getItemDateAdded(testBkmId));
 
@@ -630,17 +630,17 @@ add_test(function test_editing_item_date
 });
 
 add_test(function test_edit_item_last_modified() {
   let testURI = NetUtil.newURI("http://test_edit_item_last_modified.com");
   let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX,
                                        "Test editing item last modified");
 
   let oldModified = bmsvc.getItemLastModified(testBkmId);
-  let newModified = Date.now() + 1000;
+  let newModified = Date.now() * 1000 + 1000;
   let txn = new PlacesEditItemLastModifiedTransaction(testBkmId, newModified);
 
   txn.doTransaction();
   do_check_eq(newModified, bmsvc.getItemLastModified(testBkmId));
 
   txn.undoTransaction();
   do_check_eq(oldModified, bmsvc.getItemLastModified(testBkmId));
 
@@ -859,17 +859,17 @@ add_test(function test_aggregate_removeI
 });
 
 add_test(function test_create_item_with_childTxn() {
   let testFolder = bmsvc.createFolder(root, "Test creating an item with childTxns", bmsvc.DEFAULT_INDEX);
 
   const BOOKMARK_TITLE = "parent item";
   let testURI = NetUtil.newURI("http://test_create_item_with_childTxn.com");
   let childTxns = [];
-  let newDateAdded = Date.now() - 20000;
+  let newDateAdded = Date.now() * 1000 - 20000;
   let editDateAdddedTxn = new PlacesEditItemDateAddedTransaction(null, newDateAdded);
   childTxns.push(editDateAdddedTxn);
 
   let itemChildAnnoObj = { name: "testAnno/testInt",
                            type: Ci.nsIAnnotationService.TYPE_INT32,
                            flags: 0,
                            value: 123,
                            expires: Ci.nsIAnnotationService.EXPIRE_NEVER };
--- a/toolkit/devtools/apps/tests/debugger-protocol-helper.js
+++ b/toolkit/devtools/apps/tests/debugger-protocol-helper.js
@@ -23,18 +23,22 @@ function connect(onDone) {
     let settingsService = Cc["@mozilla.org/settingsService;1"].getService(Ci.nsISettingsService);
     settingsService.createLock().set("devtools.debugger.remote-enabled", true, null);
     // We can't use `set` callback as it is fired before shell.js code listening for this setting
     // is actually called. Same thing applies to mozsettings-changed obs notification.
     // So listen to a custom event until bug 942756 lands
     let observer = {
       observe: function (subject, topic, data) {
         Services.obs.removeObserver(observer, "debugger-server-started");
-        let transport = DebuggerClient.socketConnect("127.0.0.1", 6000);
-        startClient(transport, onDone);
+        DebuggerClient.socketConnect({
+          host: "127.0.0.1",
+          port: 6000
+        }).then(transport => {
+          startClient(transport, onDone);
+        }, e => dump("Connection failed: " + e + "\n"));
       }
     };
     Services.obs.addObserver(observer, "debugger-server-started", false);
   } else {
     // Initialize a loopback remote protocol connection
     DebuggerServer.init();
     // We need to register browser actors to have `listTabs` working
     // and also have a root actor
--- a/toolkit/devtools/client/connection-manager.js
+++ b/toolkit/devtools/client/connection-manager.js
@@ -4,20 +4,23 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {Cc, Ci, Cu} = require("chrome");
 const {setTimeout, clearTimeout} = require('sdk/timers');
 const EventEmitter = require("devtools/toolkit/event-emitter");
+const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
 Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
+DevToolsUtils.defineLazyModuleGetter(this, "Task",
+  "resource://gre/modules/Task.jsm");
 
 /**
  * Connection Manager.
  *
  * To use this module:
  * const {ConnectionManager} = require("devtools/client/connection-manager");
  *
  * # ConnectionManager
@@ -43,17 +46,18 @@ Cu.import("resource://gre/modules/devtoo
  *  . connect(transport)    Connect via transport. Expect a "connecting" event.
  *  . disconnect()          Disconnect if connected. Expect a "disconnecting" event
  *
  * Properties:
  *  . host                  IP address or hostname
  *  . port                  Port
  *  . logs                  Current logs. "newlog" event notifies new available logs
  *  . store                 Reference to a local data store (see below)
- *  . keepConnecting        Should the connection keep trying connecting
+ *  . keepConnecting        Should the connection keep trying to connect?
+ *  . encryption            Should the connection be encrypted?
  *  . status                Connection status:
  *                            Connection.Status.CONNECTED
  *                            Connection.Status.DISCONNECTED
  *                            Connection.Status.CONNECTING
  *                            Connection.Status.DISCONNECTING
  *                            Connection.Status.DESTROYED
  *
  * Events (as in event-emitter.js):
@@ -107,17 +111,17 @@ function Connection(host, port) {
   EventEmitter.decorate(this);
   this.uid = ++lastID;
   this.host = host;
   this.port = port;
   this._setStatus(Connection.Status.DISCONNECTED);
   this._onDisconnected = this._onDisconnected.bind(this);
   this._onConnected = this._onConnected.bind(this);
   this._onTimeout = this._onTimeout.bind(this);
-  this.keepConnecting = false;
+  this.resetOptions();
 }
 
 Connection.Status = {
   CONNECTED: "connected",
   DISCONNECTED: "disconnected",
   CONNECTING: "connecting",
   DISCONNECTING: "disconnecting",
   DESTROYED: "destroyed",
@@ -170,16 +174,21 @@ Connection.prototype = {
 
   set port(value) {
     if (this._port && this._port == value)
       return;
     this._port = value;
     this.emit(Connection.Events.PORT_CHANGED);
   },
 
+  resetOptions() {
+    this.keepConnecting = false;
+    this.encryption = false;
+  },
+
   disconnect: function(force) {
     if (this.status == Connection.Status.DESTROYED) {
       return;
     }
     clearTimeout(this._timeoutID);
     if (this.status == Connection.Status.CONNECTED ||
         this.status == Connection.Status.CONNECTING) {
       this.log("disconnecting");
@@ -217,40 +226,48 @@ Connection.prototype = {
     this.keepConnecting = false;
     if (this._client) {
       this._client.close();
       this._client = null;
     }
     this._setStatus(Connection.Status.DESTROYED);
   },
 
-  _clientConnect: function () {
-    let transport;
+  _getTransport: Task.async(function*() {
     if (this._customTransport) {
-      transport = this._customTransport;
-    } else {
-      if (!this.host) {
-        transport = DebuggerServer.connectPipe();
-      } else {
-        try {
-          transport = DebuggerClient.socketConnect(this.host, this.port);
-        } catch (e) {
-          // In some cases, especially on Mac, the openOutputStream call in
-          // DebuggerClient.socketConnect may throw NS_ERROR_NOT_INITIALIZED.
-          // It occurs when we connect agressively to the simulator,
-          // and keep trying to open a socket to the server being started in
-          // the simulator.
-          this._onDisconnected();
-          return;
-        }
+      return this._customTransport;
+    }
+    if (!this.host) {
+      return DebuggerServer.connectPipe();
+    }
+    let transport = yield DebuggerClient.socketConnect({
+      host: this.host,
+      port: this.port,
+      encryption: this.encryption
+    });
+    return transport;
+  }),
+
+  _clientConnect: function () {
+    this._getTransport().then(transport => {
+      if (!transport) {
+        return;
       }
-    }
-    this._client = new DebuggerClient(transport);
-    this._client.addOneTimeListener("closed", this._onDisconnected);
-    this._client.connect(this._onConnected);
+      this._client = new DebuggerClient(transport);
+      this._client.addOneTimeListener("closed", this._onDisconnected);
+      this._client.connect(this._onConnected);
+    }, e => {
+      console.error(e);
+      // In some cases, especially on Mac, the openOutputStream call in
+      // DebuggerClient.socketConnect may throw NS_ERROR_NOT_INITIALIZED.
+      // It occurs when we connect agressively to the simulator,
+      // and keep trying to open a socket to the server being started in
+      // the simulator.
+      this._onDisconnected();
+    });
   },
 
   get status() {
     return this._status
   },
 
   _setStatus: function(value) {
     if (this._status && this._status == value)
--- a/toolkit/devtools/client/dbg-client.jsm
+++ b/toolkit/devtools/client/dbg-client.jsm
@@ -367,19 +367,19 @@ DebuggerClient.Argument = function (aPos
 DebuggerClient.Argument.prototype.getArgument = function (aParams) {
   if (!(this.position in aParams)) {
     throw new Error("Bad index into params: " + this.position);
   }
   return aParams[this.position];
 };
 
 // Expose this to save callers the trouble of importing DebuggerSocket
-DebuggerClient.socketConnect = function(host, port) {
+DebuggerClient.socketConnect = function(options) {
   // Defined here instead of just copying the function to allow lazy-load
-  return DebuggerSocket.connect(host, port);
+  return DebuggerSocket.connect(options);
 };
 
 DebuggerClient.prototype = {
   /**
    * Connect to the server and start exchanging protocol messages.
    *
    * @param aOnConnected function
    *        If specified, will be called when the greeting packet is
--- a/toolkit/devtools/gcli/commands/listen.js
+++ b/toolkit/devtools/gcli/commands/listen.js
@@ -43,21 +43,24 @@ exports.items = [
         type: "number",
         get defaultValue() {
           return Services.prefs.getIntPref("devtools.debugger.chrome-debugging-port");
         },
         description: gcli.lookup("listenPortDesc"),
       }
     ],
     exec: function(args, context) {
-      var reply = debuggerServer.openListener(args.port);
-      if (!reply) {
+      var listener = debuggerServer.createListener();
+      if (!listener) {
         throw new Error(gcli.lookup("listenDisabledOutput"));
       }
 
+      listener.portOrPath = args.port;
+      listener.open();
+
       if (debuggerServer.initialized) {
         return gcli.lookupFormat("listenInitOutput", [ "" + args.port ]);
       }
 
       return gcli.lookup("listenNoInitOutput");
     },
   }
 ];
--- a/toolkit/devtools/gcli/source/lib/gcli/connectors/rdp.js
+++ b/toolkit/devtools/gcli/source/lib/gcli/connectors/rdp.js
@@ -14,16 +14,17 @@
  * limitations under the License.
  */
 
 'use strict';
 
 var Cu = require('chrome').Cu;
 
 var DebuggerClient = Cu.import('resource://gre/modules/devtools/dbg-client.jsm', {}).DebuggerClient;
+var { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
 
 var Promise = require('../util/promise').Promise;
 var Connection = require('./connectors').Connection;
 
 /**
  * What port should we use by default?
  */
 Object.defineProperty(exports, 'defaultPort', {
@@ -56,36 +57,40 @@ exports.items = [
  */
 function RdpConnection(url) {
   throw new Error('Use RdpConnection.create');
 }
 
 /**
  * Asynchronous construction
  */
-RdpConnection.create = function(url) {
+RdpConnection.create = Task.async(function*(url) {
   this.host = url;
   this.port = undefined; // TODO: Split out the port number
 
   this.requests = {};
   this.nextRequestId = 0;
 
   this._emit = this._emit.bind(this);
 
+  let transport = yield DebuggerClient.socketConnect({
+    host: this.host,
+    port: this.port
+  });
+
   return new Promise(function(resolve, reject) {
-    this.transport = DebuggerClient.socketConnect(this.host, this.port);
-    this.client = new DebuggerClient(this.transport);
+    this.client = new DebuggerClient(transport);
     this.client.connect(function() {
       this.client.listTabs(function(response) {
         this.actor = response.gcliActor;
         resolve();
       }.bind(this));
     }.bind(this));
   }.bind(this));
-};
+});
 
 RdpConnection.prototype = Object.create(Connection.prototype);
 
 RdpConnection.prototype.call = function(command, data) {
   return new Promise(function(resolve, reject) {
     var request = { to: this.actor, type: command, data: data };
 
     this.client.request(request, function(response) {
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/security/cert.js
@@ -0,0 +1,66 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+let { Ci, Cc } = require("chrome");
+let promise = require("promise");
+let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
+DevToolsUtils.defineLazyGetter(this, "localCertService", () => {
+  // Ensure PSM is initialized to support TLS sockets
+  Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
+  return Cc["@mozilla.org/security/local-cert-service;1"]
+         .getService(Ci.nsILocalCertService);
+});
+
+const localCertName = "devtools";
+
+exports.local = {
+
+  /**
+   * Get or create a new self-signed X.509 cert to represent this device for
+   * DevTools purposes over a secure transport, like TLS.
+   *
+   * The cert is stored permanently in the profile's key store after first use,
+   * and is valid for 1 year.  If an expired or otherwise invalid cert is found,
+   * it is removed and a new one is made.
+   *
+   * @return promise
+   */
+  getOrCreate() {
+    let deferred = promise.defer();
+    localCertService.getOrCreateCert(localCertName, {
+      handleCert: function(cert, rv) {
+        if (rv) {
+          deferred.reject(rv);
+          return;
+        }
+        deferred.resolve(cert);
+      }
+    });
+    return deferred.promise;
+  },
+
+  /**
+   * Remove the DevTools self-signed X.509 cert for this device.
+   *
+   * @return promise
+   */
+  remove() {
+    let deferred = promise.defer();
+    localCertService.removeCert(localCertName, {
+      handleCert: function(rv) {
+        if (rv) {
+          deferred.reject(rv);
+          return;
+        }
+        deferred.resolve();
+      }
+    });
+    return deferred.promise;
+  }
+
+};
--- a/toolkit/devtools/security/moz.build
+++ b/toolkit/devtools/security/moz.build
@@ -16,10 +16,11 @@ UNIFIED_SOURCES += [
     'LocalCertService.cpp',
 ]
 
 FAIL_ON_WARNINGS = True
 
 FINAL_LIBRARY = 'xul'
 
 EXTRA_JS_MODULES.devtools.security += [
+    'cert.js',
     'socket.js',
 ]
--- a/toolkit/devtools/security/socket.js
+++ b/toolkit/devtools/security/socket.js
@@ -2,74 +2,212 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 let { Ci, Cc, CC, Cr } = require("chrome");
+
+// Ensure PSM is initialized to support TLS sockets
+Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
+
 let Services = require("Services");
+let promise = require("promise");
 let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
-let { dumpn } = DevToolsUtils;
+let { dumpn, dumpv } = DevToolsUtils;
 loader.lazyRequireGetter(this, "DebuggerTransport",
   "devtools/toolkit/transport/transport", true);
 loader.lazyRequireGetter(this, "DebuggerServer",
   "devtools/server/main", true);
-
-DevToolsUtils.defineLazyGetter(this, "ServerSocket", () => {
-  return CC("@mozilla.org/network/server-socket;1",
-            "nsIServerSocket",
-            "initSpecialConnection");
-});
-
-DevToolsUtils.defineLazyGetter(this, "UnixDomainServerSocket", () => {
-  return CC("@mozilla.org/network/server-socket;1",
-            "nsIServerSocket",
-            "initWithFilename");
-});
+loader.lazyRequireGetter(this, "discovery",
+  "devtools/toolkit/discovery/discovery");
+loader.lazyRequireGetter(this, "cert",
+  "devtools/toolkit/security/cert");
+loader.lazyRequireGetter(this, "setTimeout", "Timer", true);
+loader.lazyRequireGetter(this, "clearTimeout", "Timer", true);
 
 DevToolsUtils.defineLazyGetter(this, "nsFile", () => {
   return CC("@mozilla.org/file/local;1", "nsIFile", "initWithPath");
 });
 
 DevToolsUtils.defineLazyGetter(this, "socketTransportService", () => {
   return Cc["@mozilla.org/network/socket-transport-service;1"]
          .getService(Ci.nsISocketTransportService);
 });
 
+DevToolsUtils.defineLazyGetter(this, "certOverrideService", () => {
+  return Cc["@mozilla.org/security/certoverride;1"]
+         .getService(Ci.nsICertOverrideService);
+});
+
+DevToolsUtils.defineLazyGetter(this, "nssErrorsService", () => {
+  return Cc["@mozilla.org/nss_errors_service;1"]
+         .getService(Ci.nsINSSErrorsService);
+});
+
+DevToolsUtils.defineLazyModuleGetter(this, "Task",
+  "resource://gre/modules/Task.jsm");
+
 const DBG_STRINGS_URI = "chrome://global/locale/devtools/debugger.properties";
 
+let DebuggerSocket = {};
+
 /**
- * Connects to a debugger server socket and returns a DebuggerTransport.
+ * Connects to a debugger server socket.
  *
  * @param host string
  *        The host name or IP address of the debugger server.
  * @param port number
  *        The port number of the debugger server.
+ * @param encryption boolean (optional)
+ *        Whether the server requires encryption.  Defaults to false.
+ * @return promise
+ *         Resolved to a DebuggerTransport instance.
  */
-function socketConnect(host, port) {
-  let s = socketTransportService.createTransport(null, 0, host, port, null);
+DebuggerSocket.connect = Task.async(function*({ host, port, encryption }) {
+  let attempt = yield _attemptTransport({ host, port, encryption });
+  if (attempt.transport) {
+    return attempt.transport; // Success
+  }
+
+  // If the server cert failed validation, store a temporary override and make
+  // a second attempt.
+  if (encryption && attempt.certError) {
+    _storeCertOverride(attempt.s, host, port);
+  } else {
+    throw new Error("Connection failed");
+  }
+
+  attempt = yield _attemptTransport({ host, port, encryption });
+  if (attempt.transport) {
+    return attempt.transport; // Success
+  }
+
+  throw new Error("Connection failed even after cert override");
+});
+
+/**
+ * Try to connect and create a DevTools transport.
+ *
+ * @return transport DebuggerTransport
+ *         A possible DevTools transport (if connection succeeded and streams
+ *         are actually alive and working)
+ * @return certError boolean
+ *         Flag noting if cert trouble caused the streams to fail
+ * @return s nsISocketTransport
+ *         Underlying socket transport, in case more details are needed.
+ */
+let _attemptTransport = Task.async(function*({ host, port, encryption }){
+  // _attemptConnect only opens the streams.  Any failures at that stage
+  // aborts the connection process immedidately.
+  let { s, input, output } = _attemptConnect({ host, port, encryption });
+
+  // Check if the input stream is alive.  If encryption is enabled, we need to
+  // watch out for cert errors by testing the input stream.
+  let { alive, certError } = yield _isInputAlive(input);
+  dumpv("Server cert accepted? " + !certError);
+
+  let transport;
+  if (alive) {
+    transport = new DebuggerTransport(input, output);
+  } else {
+    // Something went wrong, close the streams.
+    input.close();
+    output.close();
+  }
+
+  return { transport, certError, s };
+});
+
+/**
+ * Try to connect to a remote server socket.
+ *
+ * If successsful, the socket transport and its opened streams are returned.
+ * Typically, this will only fail if the host / port is unreachable.  Other
+ * problems, such as security errors, will allow this stage to succeed, but then
+ * fail later when the streams are actually used.
+ * @return s nsISocketTransport
+ *         Underlying socket transport, in case more details are needed.
+ * @return input nsIAsyncInputStream
+ *         The socket's input stream.
+ * @return output nsIAsyncOutputStream
+ *         The socket's output stream.
+ */
+function _attemptConnect({ host, port, encryption }) {
+  let s;
+  if (encryption) {
+    s = socketTransportService.createTransport(["ssl"], 1, host, port, null);
+  } else {
+    s = socketTransportService.createTransport(null, 0, host, port, null);
+  }
   // By default the CONNECT socket timeout is very long, 65535 seconds,
   // so that if we race to be in CONNECT state while the server socket is still
   // initializing, the connection is stuck in connecting state for 18.20 hours!
   s.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, 2);
 
   // openOutputStream may throw NS_ERROR_NOT_INITIALIZED if we hit some race
   // where the nsISocketTransport gets shutdown in between its instantiation and
   // the call to this method.
-  let transport;
+  let input;
+  let output;
   try {
-    transport = new DebuggerTransport(s.openInputStream(0, 0, 0),
-                                      s.openOutputStream(0, 0, 0));
+    input = s.openInputStream(0, 0, 0);
+    output = s.openOutputStream(0, 0, 0);
   } catch(e) {
-    DevToolsUtils.reportException("socketConnect", e);
+    DevToolsUtils.reportException("_attemptConnect", e);
     throw e;
   }
-  return transport;
+
+  return { s, input, output };
+}
+
+/**
+ * Check if the input stream is alive.  For an encrypted connection, it may not
+ * be if the client refuses the server's cert.  A cert error is expected on
+ * first connection to a new host because the cert is self-signed.
+ */
+function _isInputAlive(input) {
+  let deferred = promise.defer();
+  input.asyncWait({
+    onInputStreamReady(stream) {
+      try {
+        stream.available();
+        deferred.resolve({ alive: true });
+      } catch (e) {
+        try {
+          // getErrorClass may throw if you pass a non-NSS error
+          let errorClass = nssErrorsService.getErrorClass(e.result);
+          if (errorClass === Ci.nsINSSErrorsService.ERROR_CLASS_BAD_CERT) {
+            deferred.resolve({ certError: true });
+          } else {
+            deferred.reject(e);
+          }
+        } catch (nssErr) {
+          deferred.reject(e);
+        }
+      }
+    }
+  }, 0, 0, Services.tm.currentThread);
+  return deferred.promise;
+}
+
+/**
+ * To allow the connection to proceed with self-signed cert, we store a cert
+ * override.  This implies that we take on the burden of authentication for
+ * these connections.
+ */
+function _storeCertOverride(s, host, port) {
+  let cert = s.securityInfo.QueryInterface(Ci.nsISSLStatusProvider)
+              .SSLStatus.serverCert;
+  let overrideBits = Ci.nsICertOverrideService.ERROR_UNTRUSTED |
+                     Ci.nsICertOverrideService.ERROR_MISMATCH;
+  certOverrideService.rememberValidityOverride(host, port, cert, overrideBits,
+                                               true /* temporary */);
 }
 
 /**
  * Creates a new socket listener for remote connections to the DebuggerServer.
  * This helps contain and organize the parts of the server that may differ or
  * are particular to one given listener mechanism vs. another.
  */
 function SocketListener() {}
@@ -101,82 +239,161 @@ SocketListener.defaultAllowConnection = 
     DebuggerServer.closeAllListeners();
     Services.prefs.setBoolPref("devtools.debugger.remote-enabled", false);
   }
   return false;
 };
 
 SocketListener.prototype = {
 
-  /**
-   * Listens on the given port or socket file for remote debugger connections.
-   *
-   * @param portOrPath int, string
-   *        If given an integer, the port to listen on.
-   *        Otherwise, the path to the unix socket domain file to listen on.
-   */
-  open: function(portOrPath) {
-    let flags = Ci.nsIServerSocket.KeepWhenOffline;
-    // A preference setting can force binding on the loopback interface.
-    if (Services.prefs.getBoolPref("devtools.debugger.force-local")) {
-      flags |= Ci.nsIServerSocket.LoopbackOnly;
-    }
-
-    try {
-      let backlog = 4;
-      let port = Number(portOrPath);
-      if (port) {
-        this._socket = new ServerSocket(port, flags, backlog);
-      } else {
-        let file = nsFile(portOrPath);
-        if (file.exists())
-          file.remove(false);
-        this._socket = new UnixDomainServerSocket(file, parseInt("666", 8),
-                                                  backlog);
-      }
-      this._socket.asyncListen(this);
-    } catch (e) {
-      dumpn("Could not start debugging listener on '" + portOrPath + "': " + e);
-      throw Cr.NS_ERROR_NOT_AVAILABLE;
-    }
-  },
+  /* Socket Options */
 
   /**
-   * Closes the SocketListener.  Notifies the server to remove the listener from
-   * the set of active SocketListeners.
+   * The port or path to listen on.
+   *
+   * If given an integer, the port to listen on.  Use -1 to choose any available
+   * port. Otherwise, the path to the unix socket domain file to listen on.
    */
-  close: function() {
-    this._socket.close();
-    DebuggerServer._removeListener(this);
-  },
-
-  /**
-   * Gets the port that a TCP socket listener is listening on, or null if this
-   * is not a TCP socket (so there is no port).
-   */
-  get port() {
-    if (!this._socket) {
-      return null;
-    }
-    return this._socket.port;
-  },
+  portOrPath: null,
 
   /**
    * Prompt the user to accept or decline the incoming connection. The default
    * implementation is used unless this is overridden on a particular socket
    * listener instance.
    *
    * @return true if the connection should be permitted, false otherwise
    */
   allowConnection: SocketListener.defaultAllowConnection,
 
+  /**
+   * Controls whether this listener is announced via the service discovery
+   * mechanism.
+   */
+  discoverable: false,
+
+  /**
+   * Controls whether this listener's transport uses encryption.
+   */
+  encryption: false,
+
+  /**
+   * Validate that all options have been set to a supported configuration.
+   */
+  _validateOptions: function() {
+    if (this.portOrPath === null) {
+      throw new Error("Must set a port / path to listen on.");
+    }
+    if (this.discoverable && !Number(this.portOrPath)) {
+      throw new Error("Discovery only supported for TCP sockets.");
+    }
+  },
+
+  /**
+   * Listens on the given port or socket file for remote debugger connections.
+   */
+  open: function() {
+    this._validateOptions();
+    DebuggerServer._addListener(this);
+
+    let flags = Ci.nsIServerSocket.KeepWhenOffline;
+    // A preference setting can force binding on the loopback interface.
+    if (Services.prefs.getBoolPref("devtools.debugger.force-local")) {
+      flags |= Ci.nsIServerSocket.LoopbackOnly;
+    }
+
+    let self = this;
+    return Task.spawn(function*() {
+      let backlog = 4;
+      self._socket = self._createSocketInstance();
+      if (self.isPortBased) {
+        let port = Number(self.portOrPath);
+        self._socket.initSpecialConnection(port, flags, backlog);
+      } else {
+        let file = nsFile(self.portOrPath);
+        if (file.exists()) {
+          file.remove(false);
+        }
+        self._socket.initWithFilename(file, parseInt("666", 8), backlog);
+      }
+      yield self._setAdditionalSocketOptions();
+      self._socket.asyncListen(self);
+      dumpn("Socket listening on: " + (self.port || self.portOrPath));
+    }).then(() => {
+      if (this.discoverable && this.port) {
+        discovery.addService("devtools", {
+          port: this.port,
+          encryption: this.encryption
+        });
+      }
+    }).catch(e => {
+      dumpn("Could not start debugging listener on '" + this.portOrPath +
+            "': " + e);
+      this.close();
+    });
+  },
+
+  _createSocketInstance: function() {
+    if (this.encryption) {
+      return Cc["@mozilla.org/network/tls-server-socket;1"]
+             .createInstance(Ci.nsITLSServerSocket);
+    }
+    return Cc["@mozilla.org/network/server-socket;1"]
+           .createInstance(Ci.nsIServerSocket);
+  },
+
+  _setAdditionalSocketOptions: Task.async(function*() {
+    if (this.encryption) {
+      this._socket.serverCert = yield cert.local.getOrCreate();
+      this._socket.setSessionCache(false);
+      this._socket.setSessionTickets(false);
+      let requestCert = Ci.nsITLSServerSocket.REQUEST_NEVER;
+      this._socket.setRequestClientCertificate(requestCert);
+    }
+  }),
+
+  /**
+   * Closes the SocketListener.  Notifies the server to remove the listener from
+   * the set of active SocketListeners.
+   */
+  close: function() {
+    if (this.discoverable && this.port) {
+      discovery.removeService("devtools");
+    }
+    if (this._socket) {
+      this._socket.close();
+      this._socket = null;
+    }
+    DebuggerServer._removeListener(this);
+  },
+
+  /**
+   * Gets whether this listener uses a port number vs. a path.
+   */
+  get isPortBased() {
+    return !!Number(this.portOrPath);
+  },
+
+  /**
+   * Gets the port that a TCP socket listener is listening on, or null if this
+   * is not a TCP socket (so there is no port).
+   */
+  get port() {
+    if (!this.isPortBased || !this._socket) {
+      return null;
+    }
+    return this._socket.port;
+  },
+
   // nsIServerSocketListener implementation
 
   onSocketAccepted:
   DevToolsUtils.makeInfallible(function(socket, socketTransport) {
+    if (this.encryption) {
+      new SecurityObserver(socketTransport);
+    }
     if (Services.prefs.getBoolPref("devtools.debugger.prompt-connection") &&
         !this.allowConnection()) {
       return;
     }
     dumpn("New debugging connection on " +
           socketTransport.host + ":" + socketTransport.port);
 
     let input = socketTransport.openInputStream(0, 0, 0);
@@ -186,18 +403,68 @@ SocketListener.prototype = {
   }, "SocketListener.onSocketAccepted"),
 
   onStopListening: function(socket, status) {
     dumpn("onStopListening, status: " + status);
   }
 
 };
 
-// TODO: These high-level entry points will branch based on TLS vs. bare TCP as
-// part of bug 1059001.
-exports.DebuggerSocket = {
-  createListener() {
-    return new SocketListener();
+// Client must complete TLS handshake within this window (ms)
+loader.lazyGetter(this, "HANDSHAKE_TIMEOUT", () => {
+  return Services.prefs.getIntPref("devtools.remote.tls-handshake-timeout");
+});
+
+function SecurityObserver(socketTransport) {
+  this.socketTransport = socketTransport;
+  let connectionInfo = socketTransport.securityInfo
+                       .QueryInterface(Ci.nsITLSServerConnectionInfo);
+  connectionInfo.setSecurityObserver(this);
+  this._handshakeTimeout = setTimeout(this._onHandshakeTimeout.bind(this),
+                                      HANDSHAKE_TIMEOUT);
+}
+
+SecurityObserver.prototype = {
+
+  _onHandshakeTimeout() {
+    dumpv("Client failed to complete handshake");
+    this.destroy(Cr.NS_ERROR_NET_TIMEOUT);
   },
-  connect(host, port) {
-    return socketConnect(host, port);
+
+  // nsITLSServerSecurityObserver implementation
+  onHandshakeDone(socket, clientStatus) {
+    clearTimeout(this._handshakeTimeout);
+    dumpv("TLS version:    " + clientStatus.tlsVersionUsed.toString(16));
+    dumpv("TLS cipher:     " + clientStatus.cipherName);
+    dumpv("TLS key length: " + clientStatus.keyLength);
+    dumpv("TLS MAC length: " + clientStatus.macLength);
+    /*
+     * TODO: These rules should be really be set on the TLS socket directly, but
+     * this would need more platform work to expose it via XPCOM.
+     *
+     * Server *will* send hello packet when any rules below are not met, but the
+     * socket then closes after that.
+     *
+     * Enforcing cipher suites here would be a bad idea, as we want TLS
+     * cipher negotiation to work correctly.  The server already allows only
+     * Gecko's normal set of cipher suites.
+     */
+    if (clientStatus.tlsVersionUsed != Ci.nsITLSClientStatus.TLS_VERSION_1_2) {
+      this.destroy(Cr.NS_ERROR_CONNECTION_REFUSED);
+    }
+  },
+
+  destroy(result) {
+    clearTimeout(this._handshakeTimeout);
+    let connectionInfo = this.socketTransport.securityInfo
+                         .QueryInterface(Ci.nsITLSServerConnectionInfo);
+    connectionInfo.setSecurityObserver(null);
+    this.socketTransport.close(result);
+    this.socketTransport = null;
   }
+
 };
+
+DebuggerSocket.createListener = function() {
+  return new SocketListener();
+};
+
+exports.DebuggerSocket = DebuggerSocket;
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/security/tests/unit/head_dbg.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+const CC = Components.Constructor;
+
+const { devtools } =
+  Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+const { Promise: promise } =
+  Cu.import("resource://gre/modules/Promise.jsm", {});
+const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
+
+const Services = devtools.require("Services");
+const DevToolsUtils = devtools.require("devtools/toolkit/DevToolsUtils.js");
+const xpcInspector = devtools.require("xpcInspector");
+
+// We do not want to log packets by default, because in some tests,
+// we can be sending large amounts of data. The test harness has
+// trouble dealing with logging all the data, and we end up with
+// intermittent time outs (e.g. bug 775924).
+// Services.prefs.setBoolPref("devtools.debugger.log", true);
+// Services.prefs.setBoolPref("devtools.debugger.log.verbose", true);
+// Enable remote debugging for the relevant tests.
+Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
+// Fast timeout for TLS tests
+Services.prefs.setIntPref("devtools.remote.tls-handshake-timeout", 1000);
+
+function tryImport(url) {
+  try {
+    Cu.import(url);
+  } catch (e) {
+    dump("Error importing " + url + "\n");
+    dump(DevToolsUtils.safeErrorString(e) + "\n");
+    throw e;
+  }
+}
+
+tryImport("resource://gre/modules/devtools/dbg-server.jsm");
+tryImport("resource://gre/modules/devtools/dbg-client.jsm");
+
+// Convert an nsIScriptError 'aFlags' value into an appropriate string.
+function scriptErrorFlagsToKind(aFlags) {
+  var kind;
+  if (aFlags & Ci.nsIScriptError.warningFlag)
+    kind = "warning";
+  if (aFlags & Ci.nsIScriptError.exceptionFlag)
+    kind = "exception";
+  else
+    kind = "error";
+
+  if (aFlags & Ci.nsIScriptError.strictFlag)
+    kind = "strict " + kind;
+
+  return kind;
+}
+
+// Register a console listener, so console messages don't just disappear
+// into the ether.
+let errorCount = 0;
+let listener = {
+  observe: function (aMessage) {
+    errorCount++;
+    try {
+      // If we've been given an nsIScriptError, then we can print out
+      // something nicely formatted, for tools like Emacs to pick up.
+      var scriptError = aMessage.QueryInterface(Ci.nsIScriptError);
+      dump(aMessage.sourceName + ":" + aMessage.lineNumber + ": " +
+           scriptErrorFlagsToKind(aMessage.flags) + ": " +
+           aMessage.errorMessage + "\n");
+      var string = aMessage.errorMessage;
+    } catch (x) {
+      // Be a little paranoid with message, as the whole goal here is to lose
+      // no information.
+      try {
+        var string = "" + aMessage.message;
+      } catch (x) {
+        var string = "<error converting error message to string>";
+      }
+    }
+
+    // Make sure we exit all nested event loops so that the test can finish.
+    while (xpcInspector.eventLoopNestLevel > 0) {
+      xpcInspector.exitNestedEventLoop();
+    }
+
+    // Print in most cases, but ignore the "strict" messages
+    if (!(aMessage.flags & Ci.nsIScriptError.strictFlag)) {
+      do_print("head_dbg.js got console message: " + string + "\n");
+    }
+  }
+};
+
+let consoleService = Cc["@mozilla.org/consoleservice;1"]
+                     .getService(Ci.nsIConsoleService);
+consoleService.registerListener(listener);
+
+/**
+ * Initialize the testing debugger server.
+ */
+function initTestDebuggerServer() {
+  DebuggerServer.registerModule("xpcshell-test/testactors");
+  DebuggerServer.init();
+}
--- a/toolkit/devtools/security/tests/unit/test_cert.js
+++ b/toolkit/devtools/security/tests/unit/test_cert.js
@@ -1,17 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-const { utils: Cu, classes: Cc, interfaces: Ci } = Components;
-
-const { Promise: promise } =
-  Cu.import("resource://gre/modules/Promise.jsm", {});
 const certService = Cc["@mozilla.org/security/local-cert-service;1"]
                     .getService(Ci.nsILocalCertService);
 
 const gNickname = "devtools";
 
 function run_test() {
   // Need profile dir to store the key / cert
   do_get_profile();
@@ -44,17 +40,17 @@ function removeCert(nickname) {
       }
       deferred.resolve();
     }
   });
   return deferred.promise;
 }
 
 add_task(function*() {
-  // No master password, so prompt required here
+  // No master password, so no prompt required here
   ok(!certService.loginPromptRequired);
 
   let certA = yield getOrCreateCert(gNickname);
   equal(certA.nickname, gNickname);
 
   // Getting again should give the same cert
   let certB = yield getOrCreateCert(gNickname);
   equal(certB.nickname, gNickname);
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/security/tests/unit/test_encryption.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test basic functionality of DevTools client and server TLS encryption mode
+function run_test() {
+  // Need profile dir to store the key / cert
+  do_get_profile();
+  // Ensure PSM is initialized
+  Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
+  run_next_test();
+}
+
+function connectClient(client) {
+  let deferred = promise.defer();
+  client.connect(() => {
+    client.listTabs(deferred.resolve);
+  });
+  return deferred.promise;
+}
+
+add_task(function*() {
+  initTestDebuggerServer();
+});
+
+// Client w/ encryption connects successfully to server w/ encryption
+add_task(function*() {
+  equal(DebuggerServer.listeningSockets, 0, "0 listening sockets");
+
+  let listener = DebuggerServer.createListener();
+  ok(listener, "Socket listener created");
+  listener.portOrPath = -1 /* any available port */;
+  listener.allowConnection = () => true;
+  listener.encryption = true;
+  yield listener.open();
+  equal(DebuggerServer.listeningSockets, 1, "1 listening socket");
+
+  let transport = yield DebuggerClient.socketConnect({
+    host: "127.0.0.1",
+    port: listener.port,
+    encryption: true
+  });
+  ok(transport, "Client transport created");
+
+  let client = new DebuggerClient(transport);
+  let onUnexpectedClose = () => {
+    do_throw("Closed unexpectedly");
+  };
+  client.addListener("closed", onUnexpectedClose);
+  yield connectClient(client);
+
+  // Send a message the server will echo back
+  let message = "secrets";
+  let reply = yield client.request({
+    to: "root",
+    type: "echo",
+    message
+  });
+  equal(reply.message, message, "Encrypted echo matches");
+
+  client.removeListener("closed", onUnexpectedClose);
+  transport.close();
+  listener.close();
+  equal(DebuggerServer.listeningSockets, 0, "0 listening sockets");
+});
+
+// Client w/o encryption fails to connect to server w/ encryption
+add_task(function*() {
+  equal(DebuggerServer.listeningSockets, 0, "0 listening sockets");
+
+  let listener = DebuggerServer.createListener();
+  ok(listener, "Socket listener created");
+  listener.portOrPath = -1 /* any available port */;
+  listener.allowConnection = () => true;
+  listener.encryption = true;
+  yield listener.open();
+  equal(DebuggerServer.listeningSockets, 1, "1 listening socket");
+
+  try {
+    yield DebuggerClient.socketConnect({
+      host: "127.0.0.1",
+      port: listener.port
+      // encryption: false is the default
+    });
+  } catch(e) {
+    ok(true, "Client failed to connect as expected");
+    listener.close();
+    equal(DebuggerServer.listeningSockets, 0, "0 listening sockets");
+    return;
+  }
+
+  do_throw("Connection unexpectedly succeeded");
+});
+
+add_task(function*() {
+  DebuggerServer.destroy();
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/security/tests/unit/testactors.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { ActorPool, appendExtraActors, createExtraActors } =
+  require("devtools/server/actors/common");
+const { RootActor } = require("devtools/server/actors/root");
+const { ThreadActor } = require("devtools/server/actors/script");
+const { DebuggerServer } = require("devtools/server/main");
+const promise = require("promise");
+
+var gTestGlobals = [];
+DebuggerServer.addTestGlobal = function(aGlobal) {
+  gTestGlobals.push(aGlobal);
+};
+
+// A mock tab list, for use by tests. This simply presents each global in
+// gTestGlobals as a tab, and the list is fixed: it never calls its
+// onListChanged handler.
+//
+// As implemented now, we consult gTestGlobals when we're constructed, not
+// when we're iterated over, so tests have to add their globals before the
+// root actor is created.
+function TestTabList(aConnection) {
+  this.conn = aConnection;
+
+  // An array of actors for each global added with
+  // DebuggerServer.addTestGlobal.
+  this._tabActors = [];
+
+  // A pool mapping those actors' names to the actors.
+  this._tabActorPool = new ActorPool(aConnection);
+
+  for (let global of gTestGlobals) {
+    let actor = new TestTabActor(aConnection, global);
+    actor.selected = false;
+    this._tabActors.push(actor);
+    this._tabActorPool.addActor(actor);
+  }
+  if (this._tabActors.length > 0) {
+    this._tabActors[0].selected = true;
+  }
+
+  aConnection.addActorPool(this._tabActorPool);
+}
+
+TestTabList.prototype = {
+  constructor: TestTabList,
+  getList: function () {
+    return promise.resolve([tabActor for (tabActor of this._tabActors)]);
+  }
+};
+
+function createRootActor(aConnection) {
+  let root = new RootActor(aConnection, {
+    tabList: new TestTabList(aConnection),
+    globalActorFactories: DebuggerServer.globalActorFactories
+  });
+  root.applicationType = "xpcshell-tests";
+  return root;
+}
+
+function TestTabActor(aConnection, aGlobal) {
+  this.conn = aConnection;
+  this._global = aGlobal;
+  this._threadActor = new ThreadActor(this, this._global);
+  this.conn.addActor(this._threadActor);
+  this._attached = false;
+  this._extraActors = {};
+}
+
+TestTabActor.prototype = {
+  constructor: TestTabActor,
+  actorPrefix: "TestTabActor",
+
+  get window() {
+    return { wrappedJSObject: this._global };
+  },
+
+  get url() {
+    return this._global.__name;
+  },
+
+  form: function() {
+    let response = { actor: this.actorID, title: this._global.__name };
+
+    // Walk over tab actors added by extensions and add them to a new ActorPool.
+    let actorPool = new ActorPool(this.conn);
+    this._createExtraActors(DebuggerServer.tabActorFactories, actorPool);
+    if (!actorPool.isEmpty()) {
+      this._tabActorPool = actorPool;
+      this.conn.addActorPool(this._tabActorPool);
+    }
+
+    this._appendExtraActors(response);
+
+    return response;
+  },
+
+  onAttach: function(aRequest) {
+    this._attached = true;
+
+    let response = { type: "tabAttached", threadActor: this._threadActor.actorID };
+    this._appendExtraActors(response);
+
+    return response;
+  },
+
+  onDetach: function(aRequest) {
+    if (!this._attached) {
+      return { "error":"wrongState" };
+    }
+    return { type: "detached" };
+  },
+
+  /* Support for DebuggerServer.addTabActor. */
+  _createExtraActors: createExtraActors,
+  _appendExtraActors: appendExtraActors
+};
+
+TestTabActor.prototype.requestTypes = {
+  "attach": TestTabActor.prototype.onAttach,
+  "detach": TestTabActor.prototype.onDetach
+};
+
+exports.register = function(handle) {
+  handle.setRootActor(createRootActor);
+};
+
+exports.unregister = function(handle) {
+  handle.setRootActor(null);
+};
--- a/toolkit/devtools/security/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/security/tests/unit/xpcshell.ini
@@ -1,6 +1,10 @@
 [DEFAULT]
-head =
+head = head_dbg.js
 tail =
 skip-if = toolkit == 'android'
 
+support-files=
+  testactors.js
+
 [test_cert.js]
+[test_encryption.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/actors/animation.js
@@ -0,0 +1,273 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Set of actors that expose the Web Animations API to devtools protocol clients.
+ *
+ * The |Animations| actor is the main entry point. It is used to discover
+ * animation players on given nodes.
+ * There should only be one instance per debugger server.
+ *
+ * The |AnimationPlayer| actor provides attributes and methods to inspect an
+ * animation as well as pause/resume/seek it.
+ *
+ * The Web Animation spec implementation is ongoing in Gecko, and so this set
+ * of actors should evolve when the implementation progresses.
+ *
+ * References:
+ * - WebAnimation spec:
+ *   http://w3c.github.io/web-animations/
+ * - WebAnimation WebIDL files:
+ *   /dom/webidl/Animation*.webidl
+ */
+
+const {ActorClass, Actor,
+       FrontClass, Front,
+       Arg, method, RetVal} = require("devtools/server/protocol");
+const {NodeActor} = require("devtools/server/actors/inspector");
+
+/**
+ * The AnimationPlayerActor provides information about a given animation: its
+ * startTime, currentTime, current state, etc.
+ *
+ * Since the state of a player changes as the animation progresses it is often
+ * useful to call getCurrentState at regular intervals to get the current state.
+ *
+ * This actor also allows playing and pausing the animation.
+ */
+let AnimationPlayerActor = ActorClass({
+  typeName: "animationplayer",
+
+  /**
+   * @param {AnimationsActor} The main AnimationsActor instance
+   * @param {AnimationPlayer} The player object returned by getAnimationPlayers
+   * @param {DOMNode} The node targeted by this player
+   * @param {Number} Temporary work-around used to retrieve duration and
+   * iteration count from computed-style rather than from waapi. This is needed
+   * to know which duration to get, in case there are multiple css animations
+   * applied to the same node.
+   */
+  initialize: function(animationsActor, player, node, playerIndex) {
+    this.player = player;
+    this.node = node;
+    this.playerIndex = playerIndex;
+    this.styles = node.ownerDocument.defaultView.getComputedStyle(node);
+    Actor.prototype.initialize.call(this, animationsActor.conn);
+  },
+
+  destroy: function() {
+    this.player = this.node = this.styles = null;
+    Actor.prototype.destroy.call(this);
+  },
+
+  /**
+   * Release the actor, when it isn't needed anymore.
+   * Protocol.js uses this release method to call the destroy method.
+   */
+  release: method(function() {}, {release: true}),
+
+  form: function(detail) {
+    if (detail === "actorid") {
+      return this.actorID;
+    }
+
+    let data = this.getCurrentState();
+    data.actor = this.actorID;
+
+    return data;
+  },
+
+  /**
+   * Get the animation duration from this player, in milliseconds.
+   * Note that the Web Animations API doesn't yet offer a way to retrieve this
+   * directly from the AnimationPlayer object, so for now, a duration is only
+   * returned if found in the node's computed styles.
+   * @return {Number}
+   */
+  getDuration: function() {
+    let durationText;
+    if (this.styles.animationDuration !== "0s") {
+      durationText = this.styles.animationDuration;
+    } else if (this.styles.transitionDuration !== "0s") {
+      durationText = this.styles.transitionDuration;
+    } else {
+      return null;
+    }
+
+    if (durationText.indexOf(",") !== -1) {
+      durationText = durationText.split(",")[this.playerIndex];
+    }
+
+    return parseFloat(durationText) * 1000;
+  },
+
+  /**
+   * Get the animation iteration count for this player. That is, how many times
+   * is the animation scheduled to run.
+   * Note that the Web Animations API doesn't yet offer a way to retrieve this
+   * directly from the AnimationPlayer object, so for now, check for
+   * animationIterationCount in the node's computed styles, and return that.
+   * This style property defaults to 1 anyway.
+   * @return {Number}
+   */
+  getIterationCount: function() {
+    let iterationText = this.styles.animationIterationCount;
+    if (iterationText.indexOf(",") !== -1) {
+      iterationText = iterationText.split(",")[this.playerIndex];
+    }
+
+    return parseInt(iterationText, 10);
+  },
+
+  /**
+   * Get the current state of the AnimationPlayer (currentTime, playState, ...).
+   * Note that the initial state is returned as the form of this actor when it
+   * is initialized.
+   * @return {Object}
+   */
+  getCurrentState: method(function() {
+    return {
+      /**
+       * Return the player's current startTime value.
+       * Will be null whenever the animation is paused or waiting to start.
+       */
+      startTime: this.player.startTime,
+      currentTime: this.player.currentTime,
+      playState: this.player.playState,
+      name: this.player.source.effect.name,
+      duration: this.getDuration(),
+      iterationCount: this.getIterationCount(),
+      /**
+       * Is the animation currently running on the compositor. This is important for
+       * developers to know if their animation is hitting the fast path or not.
+       * Currently this will only be true for Firefox OS though (where we have
+       * compositor animations enabled).
+       * Returns false whenever the animation is paused as it is taken off the
+       * compositor then.
+       */
+      isRunningOnCompositor: this.player.isRunningOnCompositor
+    };
+  }, {
+    request: {},
+    response: {
+      data: RetVal("json")
+    }
+  }),
+
+  /**
+   * Pause the player.
+   */
+  pause: method(function() {
+    this.player.pause();
+  }, {
+    request: {},
+    response: {}
+  }),
+
+  /**
+   * Play the player.
+   */
+  play: method(function() {
+    this.player.play();
+  }, {
+    request: {},
+    response: {}
+  })
+});
+
+let AnimationPlayerFront = FrontClass(AnimationPlayerActor, {
+  initialize: function(conn, form, detail, ctx) {
+    Front.prototype.initialize.call(this, conn, form, detail, ctx);
+  },
+
+  form: function(form, detail) {
+    if (detail === "actorid") {
+      this.actorID = form;
+      return;
+    }
+    this._form = form;
+  },
+
+  destroy: function() {
+    Front.prototype.destroy.call(this);
+  },
+
+  /**
+   * Getter for the initial state of the player. Up to date states can be
+   * retrieved by calling the getCurrentState method.
+   */
+  get initialState() {
+    return {
+      startTime: this._form.startTime,
+      currentTime: this._form.currentTime,
+      playState: this._form.playState,
+      name: this._form.name,
+      duration: this._form.duration,
+      iterationCount: this._form.iterationCount,
+      isRunningOnCompositor: this._form.isRunningOnCompositor
+    }
+  }
+});
+
+/**
+ * The Animations actor lists animation players for a given node.
+ */
+let AnimationsActor = exports.AnimationsActor = ActorClass({
+  typeName: "animations",
+
+  initialize: function(conn, tabActor) {
+    Actor.prototype.initialize.call(this, conn);
+  },
+
+  destroy: function() {
+    Actor.prototype.destroy.call(this);
+  },
+
+  /**
+   * Since AnimationsActor doesn't have a protocol.js parent actor that takes
+   * care of its lifetime, implementing disconnect is required to cleanup.
+   */
+  disconnect: function() {
+    this.destroy();
+  },
+
+  /**
+   * Retrieve the list of AnimationPlayerActor actors corresponding to
+   * currently running animations for a given node.
+   * @param {NodeActor} nodeActor The NodeActor type is defined in
+   * /toolkit/devtools/server/actors/inspector
+   */
+  getAnimationPlayersForNode: method(function(nodeActor) {
+    let players = nodeActor.rawNode.getAnimationPlayers();
+
+    let actors = [];
+    for (let i = 0; i < players.length; i ++) {
+      // XXX: for now the index is passed along as the AnimationPlayerActor uses
+      // it to retrieve animation information from CSS.
+      actors.push(AnimationPlayerActor(this, players[i], nodeActor.rawNode, i));
+    }
+
+    return actors;
+  }, {
+    request: {
+      actorID: Arg(0, "domnode")
+    },
+    response: {
+      players: RetVal("array:animationplayer")
+    }
+  })
+});
+
+let AnimationsFront = exports.AnimationsFront = FrontClass(AnimationsActor, {
+  initialize: function(client, {animationsActor}) {
+    Front.prototype.initialize.call(this, client, {actor: animationsActor});
+    this.manage(this);
+  },
+
+  destroy: function() {
+    Front.prototype.destroy.call(this);
+  }
+});
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -58,38 +58,17 @@ Debugger.Object.prototype.getPromiseStat
   };
 };
 
 /**
  * A BreakpointActorMap is a map from locations to instances of BreakpointActor.
  */
 function BreakpointActorMap() {
   this._size = 0;
-
-  // If we have a whole-line breakpoint set at LINE in URL, then
-  //
-  //   this._wholeLineBreakpoints[URL][LINE]
-  //
-  // is an object
-  //
-  //   { url, line[, actor] }
-  //
-  // where the `actor` property is optional.
-  this._wholeLineBreakpoints = Object.create(null);
-
-  // If we have a breakpoint set at LINE, COLUMN in URL, then
-  //
-  //   this._breakpoints[URL][LINE][COLUMN]
-  //
-  // is an object
-  //
-  //   { url, line, column[, actor] }
-  //
-  // where the `actor` property is optional.
-  this._breakpoints = Object.create(null);
+  this._actors = {};
 }
 
 BreakpointActorMap.prototype = {
   /**
    * Return the number of instances of BreakpointActor in this instance of
    * BreakpointActorMap.
    *
    * @returns Number
@@ -106,64 +85,56 @@ BreakpointActorMap.prototype = {
    *
    * @param Object query
    *        An optional object with the following properties:
    *        - source (optional)
    *        - line (optional, requires source)
    *        - column (optional, requires line)
    */
   findActors: function* (query = {}) {
-    if (query.column != null) {
-      dbg_assert(query.line != null);
-    }
-    if (query.line != null) {
-      dbg_assert(query.source != null);
-      dbg_assert(query.source.actor != null);
-    }
-
-    let actor = query.source ? query.source.actor : null;
-    for (let actor of this._iterActors(actor)) {
-      for (let line of this._iterLines(actor, query.line)) {
-        // Always yield whole line breakpoints first. See comment in
-        // |BreakpointActorMap.prototype.getActor|.
-        if (query.column == null
-            && this._wholeLineBreakpoints[actor]
-            && this._wholeLineBreakpoints[actor][line]) {
-          yield this._wholeLineBreakpoints[actor][line];
+    function* findKeys(object, key) {
+      if (key !== undefined) {
+        if (key in object) {
+          yield key;
         }
-        for (let column of this._iterColumns(actor, line, query.column)) {
-          yield this._breakpoints[actor][line][column];
+      }
+      else {
+        for (let key of Object.keys(object)) {
+          yield key;
         }
       }
     }
+
+    query.actor = query.source ? query.source.actor : undefined;
+    query.beginColumn = query.column ? query.column : undefined;
+    query.endColumn = query.column ? query.column + 1 : undefined;
+
+    for (let actor of findKeys(this._actors, query.actor))
+    for (let line of findKeys(this._actors[actor], query.line))
+    for (let beginColumn of findKeys(this._actors[actor][line], query.beginColumn))
+    for (let endColumn of findKeys(this._actors[actor][line][beginColumn], query.endColumn)) {
+      yield this._actors[actor][line][beginColumn][endColumn];
+    }
   },
 
   /**
    * Return the instance of BreakpointActor at the given location in this
    * instance of BreakpointActorMap.
    *
    * @param Object location
    *        An object with the following properties:
    *        - source
    *        - line
    *        - column (optional)
    *
    * @returns BreakpointActor actor
    *          The instance of BreakpointActor at the given location.
    */
   getActor: function (location) {
-    let { source: { actor }, line, column } = location;
-
-    dbg_assert(actor != null);
-    dbg_assert(line != null);
     for (let actor of this.findActors(location)) {
-      // We will get whole line breakpoints before individual columns, so just
-      // return the first one and if they didn't specify a column then they will
-      // get the whole line breakpoint, and otherwise we will find the correct
-      // one.
       return actor;
     }
 
     return null;
   },
 
   /**
    * Set the given instance of BreakpointActor to the given location in this
@@ -176,147 +147,67 @@ BreakpointActorMap.prototype = {
    *        - column (optional)
    *
    * @param BreakpointActor actor
    *        The instance of BreakpointActor to be set to the given location.
    */
   setActor: function (location, actor) {
     let { source, line, column } = location;
 
-    if (column != null) {
-      if (!this._breakpoints[source.actor]) {
-        this._breakpoints[source.actor] = [];
-      }
-      if (!this._breakpoints[source.actor][line]) {
-        this._breakpoints[source.actor][line] = [];
-      }
-
-      if (!this._breakpoints[source.actor][line][column]) {
-        this._breakpoints[source.actor][line][column] = actor;
-        this._size++;
-      }
-      return this._breakpoints[source.actor][line][column];
-    } else {
-      // Add a breakpoint that breaks on the whole line.
-      if (!this._wholeLineBreakpoints[source.actor]) {
-        this._wholeLineBreakpoints[source.actor] = [];
-      }
-
-      if (!this._wholeLineBreakpoints[source.actor][line]) {
-        this._wholeLineBreakpoints[source.actor][line] = actor;
-        this._size++;
-      }
-      return this._wholeLineBreakpoints[source.actor][line];
-    }
+    let beginColumn = column ? column : 0;
+    let endColumn = column ? column + 1 : Infinity;
+
+    if (!this._actors[source.actor]) {
+      this._actors[source.actor] = [];
+    }
+    if (!this._actors[source.actor][line]) {
+      this._actors[source.actor][line] = [];
+    }
+    if (!this._actors[source.actor][line][beginColumn]) {
+      this._actors[source.actor][line][beginColumn] = [];
+    }
+    if (!this._actors[source.actor][line][beginColumn][endColumn]) {
+      ++this._size;
+    }
+    this._actors[source.actor][line][beginColumn][endColumn] = actor;
   },
 
   /**
    * Delete the instance of BreakpointActor from the given location in this
    * instance of BreakpointActorMap.
    *
    * @param Object location
    *        An object with the following properties:
    *        - source
    *        - line
    *        - column (optional)
    */
   deleteActor: function (location) {
-    let { source: { actor }, line, column } = location;
-
-    if (column != null) {
-      if (this._breakpoints[actor]) {
-        if (this._breakpoints[actor][line]) {
-          if (this._breakpoints[actor][line][column]) {
-            delete this._breakpoints[actor][line][column];
-            this._size--;
-
-            // If this was the last breakpoint on this line, delete the line from
-            // `this._breakpoints[url]` as well. Otherwise `_iterLines` will yield
-            // this line even though we no longer have breakpoints on
-            // it. Furthermore, we use Object.keys() instead of just checking
-            // `this._breakpoints[url].length` directly, because deleting
-            // properties from sparse arrays doesn't update the `length` property
-            // like adding them does.
-            if (Object.keys(this._breakpoints[actor][line]).length === 0) {
-              delete this._breakpoints[actor][line];
-            }
+    let { source, line, column } = location;
+
+    let beginColumn = column ? column : 0;
+    let endColumn = column ? column + 1 : Infinity;
+
+    if (this._actors[source.actor]) {
+      if (this._actors[source.actor][line]) {
+        if (this._actors[source.actor][line][beginColumn]) {
+          if (this._actors[source.actor][line][beginColumn][endColumn]) {
+            --this._size;
+          }
+          delete this._actors[source.actor][line][beginColumn][endColumn];
+          if (Object.keys(this._actors[source.actor][line][beginColumn]).length === 0) {
+            delete this._actors[source.actor][line][beginColumn];
           }
         }
-      }
-    } else {
-      if (this._wholeLineBreakpoints[actor]) {
-        if (this._wholeLineBreakpoints[actor][line]) {
-          delete this._wholeLineBreakpoints[actor][line];
-          this._size--;
+        if (Object.keys(this._actors[source.actor][line]).length === 0) {
+          delete this._actors[source.actor][line];
         }
       }
     }
-  },
-
-  _iterActors: function* (aActor) {
-    if (aActor) {
-      if (this._breakpoints[aActor] || this._wholeLineBreakpoints[aActor]) {
-        yield aActor;
-      }
-    } else {
-      for (let actor of Object.keys(this._wholeLineBreakpoints)) {
-        yield actor;
-      }
-      for (let actor of Object.keys(this._breakpoints)) {
-        if (actor in this._wholeLineBreakpoints) {
-          continue;
-        }
-        yield actor;
-      }
-    }
-  },
-
-  _iterLines: function* (aActor, aLine) {
-    if (aLine != null) {
-      if ((this._wholeLineBreakpoints[aActor]
-           && this._wholeLineBreakpoints[aActor][aLine])
-          || (this._breakpoints[aActor] && this._breakpoints[aActor][aLine])) {
-        yield aLine;
-      }
-    } else {
-      const wholeLines = this._wholeLineBreakpoints[aActor]
-        ? Object.keys(this._wholeLineBreakpoints[aActor])
-        : [];
-      const columnLines = this._breakpoints[aActor]
-        ? Object.keys(this._breakpoints[aActor])
-        : [];
-
-      const lines = wholeLines.concat(columnLines).sort();
-
-      let lastLine;
-      for (let line of lines) {
-        if (line === lastLine) {
-          continue;
-        }
-        yield line;
-        lastLine = line;
-      }
-    }
-  },
-
-  _iterColumns: function* (aActor, aLine, aColumn) {
-    if (!this._breakpoints[aActor] || !this._breakpoints[aActor][aLine]) {
-      return;
-    }
-
-    if (aColumn != null) {
-      if (this._breakpoints[aActor][aLine][aColumn]) {
-        yield aColumn;
-      }
-    } else {
-      for (let column in this._breakpoints[aActor][aLine]) {
-        yield column;
-      }
-    }
-  },
+  }
 };
 
 exports.BreakpointActorMap = BreakpointActorMap;
 
 /**
  * Keeps track of persistent sources across reloads and ties different
  * source instances to the same actor id so that things like
  * breakpoints survive reloads. ThreadSources uses this to force the
@@ -1288,17 +1179,17 @@ ThreadActor.prototype = {
    */
   _breakOnEnter: function(script) {
     let offsets = script.getAllOffsets();
     let sourceActor = this.sources.source({ source: script.source });
 
     for (let line = 0, n = offsets.length; line < n; line++) {
       if (offsets[line]) {
         let location = { line: line };
-        let resp = sourceActor._setBreakpoint(location);
+        let resp = sourceActor.setBreakpoint(location);
         dbg_assert(!resp.actualLocation, "No actualLocation should be returned");
         if (resp.error) {
           reportError(new Error("Unable to set breakpoint on event listener"));
           return;
         }
         let bpActor = this.breakpointActorMap.getActor({
           source: sourceActor.form(),
           line: location.line
@@ -2111,17 +2002,17 @@ ThreadActor.prototype = {
     // Set any stored breakpoints.
 
     let endLine = aScript.startLine + aScript.lineCount - 1;
     let source = this.sources.source({ source: aScript.source });
     for (let bpActor of this.breakpointActorMap.findActors({ source: source.form() })) {
       // Limit the search to the line numbers contained in the new script.
       if (bpActor.location.line >= aScript.startLine
           && bpActor.location.line <= endLine) {
-        source._setBreakpoint(bpActor.location, aScript);
+        source.setBreakpoint(bpActor.location, aScript);
       }
     }
 
     return true;
   },
 
 
   /**
@@ -2795,17 +2686,17 @@ SourceActor.prototype = {
       else {
         return this._createBreakpoint(genLoc, originalLoc, aRequest.condition);
       }
     });
   },
 
   _createBreakpoint: function(loc, originalLoc, condition) {
     return resolve(null).then(() => {
-      return this._setBreakpoint({
+      return this.setBreakpoint({
         line: loc.line,
         column: loc.column,
         condition: condition
       });
     }).then(response => {
       var actual = response.actualLocation;
       if (actual) {
         if (this.source) {
@@ -2988,17 +2879,17 @@ SourceActor.prototype = {
    *
    * @param object aLocation
    *        The location of the breakpoint (in the generated source, if source
    *        mapping).
    * @param Debugger.Script aOnlyThisScript [optional]
    *        If provided, only set breakpoints in this Debugger.Script, and
    *        nowhere else.
    */
-  _setBreakpoint: function (aLocation, aOnlyThisScript=null) {
+  setBreakpoint: function (aLocation, aOnlyThisScript=null) {
     const location = {
       source: this.form(),
       line: aLocation.line,
       column: aLocation.column,
       condition: aLocation.condition
     };
     const actor = location.actor = this._getOrCreateBreakpointActor(location);
 
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -506,16 +506,21 @@ var DebuggerServer = {
     });
     if ("nsIProfiler" in Ci) {
       this.registerModule("devtools/server/actors/profiler", {
         prefix: "profiler",
         constructor: "ProfilerActor",
         type: { global: true, tab: true }
       });
     }
+    this.registerModule("devtools/server/actors/animation", {
+      prefix: "animations",
+      constructor: "AnimationsActor",
+      type: { global: true, tab: true }
+    });
   },
 
   /**
    * Passes a set of options to the BrowserAddonActors for the given ID.
    *
    * @param aId string
    *        The ID of the add-on to pass the options to
    * @param aOptions object
@@ -537,38 +542,44 @@ var DebuggerServer = {
     return all(promises);
   },
 
   get listeningSockets() {
     return this._listeners.length;
   },
 
   /**
-   * Listens on the given port or socket file for remote debugger connections.
+   * Creates a socket listener for remote debugger connections.
    *
-   * @param portOrPath int, string
-   *        If given an integer, the port to listen on.
-   *        Otherwise, the path to the unix socket domain file to listen on.
+   * After calling this, set some socket options, such as the port / path to
+   * listen on, and then call |open| on the listener.
+   *
+   * See SocketListener in toolkit/devtools/security/socket.js for available
+   * options.
+   *
    * @return SocketListener
-   *         A SocketListener instance that is already opened is returned.  This
-   *         single listener can be closed at any later time by calling |close|
-   *         on the SocketListener.  If a SocketListener could not be opened, an
-   *         error is thrown.  If remote connections are disabled, undefined is
-   *         returned.
+   *         A SocketListener instance that is waiting to be configured and
+   *         opened is returned.  This single listener can be closed at any
+   *         later time by calling |close| on the SocketListener.  If remote
+   *         connections are disabled, an error is thrown.
    */
-  openListener: function(portOrPath) {
+  createListener: function() {
     if (!Services.prefs.getBoolPref("devtools.debugger.remote-enabled")) {
-      return;
+      throw new Error("Can't create listener, remote debugging disabled");
     }
     this._checkInit();
+    return DebuggerSocket.createListener();
+  },
 
-    let listener = DebuggerSocket.createListener();
-    listener.open(portOrPath);
+  /**
+   * Add a SocketListener instance to the server's set of active
+   * SocketListeners.  This is called by a SocketListener after it is opened.
+   */
+  _addListener: function(listener) {
     this._listeners.push(listener);
-    return listener;
   },
 
   /**
    * Remove a SocketListener instance from the server's set of active
    * SocketListeners.  This is called by a SocketListener after it is closed.
    */
   _removeListener: function(listener) {
     this._listeners = this._listeners.filter(l => l !== listener);
--- a/toolkit/devtools/server/moz.build
+++ b/toolkit/devtools/server/moz.build
@@ -29,16 +29,17 @@ EXTRA_JS_MODULES.devtools.server += [
     'child.js',
     'content-globals.js',
     'main.js',
     'protocol.js',
 ]
 
 EXTRA_JS_MODULES.devtools.server.actors += [
     'actors/actor-registry.js',
+    'actors/animation.js',
     'actors/call-watcher.js',
     'actors/canvas.js',
     'actors/child-process.js',
     'actors/childtab.js',
     'actors/common.js',
     'actors/csscoverage.js',
     'actors/device.js',
     'actors/eventlooplag.js',
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/animation.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<style>
+  .simple-animation {
+    display: inline-block;
+
+    width: 150px;
+    height: 150px;
+    border-radius: 50%;
+    background: red;
+
+    animation: move 2s infinite;
+  }
+
+  .multiple-animations {
+    display: inline-block;
+
+    width: 150px;
+    height: 150px;
+    border-radius: 50%;
+    background: #eee;
+
+    animation: move 2s infinite, glow 1s 5;
+  }
+
+  .transition {
+    display: inline-block;
+
+    width: 150px;
+    height: 150px;
+    border-radius: 50%;
+    background: #f06;
+
+    transition: width 5s;
+  }
+  .transition.get-round {
+    width: 200px;
+  }
+
+  .short-animation {
+    display: inline-block;
+
+    width: 150px;
+    height: 150px;
+    border-radius: 50%;
+    background: purple;
+
+    animation: move 1s;
+  }
+
+  @keyframes move {
+    100% {
+      transform: translateY(100px);
+    }
+  }
+
+  @keyframes glow {
+    100% {
+      background: yellow;
+    }
+  }
+</style>
+<div class="not-animated"></div>
+<div class="simple-animation"></div>
+<div class="multiple-animations"></div>
+<div class="transition"></div>
+<div class="short-animation"></div>
+<script type="text/javascript">
+  // Get the transition started when the page loads
+  var players;
+  addEventListener("load", function() {
+    document.querySelector(".transition").classList.add("get-round");
+  });
+</script>
--- a/toolkit/devtools/server/tests/browser/browser.ini
+++ b/toolkit/devtools/server/tests/browser/browser.ini
@@ -1,23 +1,28 @@
 [DEFAULT]
 skip-if = e10s # Bug ?????? - devtools tests disabled with e10s
 subsuite = devtools
 support-files =
   head.js
+  animation.html
   navigate-first.html
   navigate-second.html
   storage-dynamic-windows.html
   storage-listings.html
   storage-unsecured-iframe.html
   storage-updates.html
   storage-secured-iframe.html
   timeline-iframe-child.html
   timeline-iframe-parent.html
 
+[browser_animation_actors_01.js]
+[browser_animation_actors_02.js]
+[browser_animation_actors_03.js]
+[browser_animation_actors_04.js]
 [browser_navigateEvents.js]
 [browser_storage_dynamic_windows.js]
 [browser_storage_listings.js]
 [browser_storage_updates.js]
 [browser_timeline.js]
 skip-if = buildapp == 'mulet'
 [browser_timeline_actors.js]
 skip-if = buildapp == 'mulet'
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/browser_animation_actors_01.js
@@ -0,0 +1,40 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Simple checks for the AnimationsActor
+
+const {AnimationsFront} = require("devtools/server/actors/animation");
+const {InspectorFront} = require("devtools/server/actors/inspector");
+
+add_task(function*() {
+  let doc = yield addTab("data:text/html;charset=utf-8,<title>test</title><div></div>");
+
+  initDebuggerServer();
+  let client = new DebuggerClient(DebuggerServer.connectPipe());
+  let form = yield connectDebuggerClient(client);
+  let inspector = InspectorFront(client, form);
+  let walker = yield inspector.getWalker();
+  let front = AnimationsFront(client, form);
+
+  ok(front, "The AnimationsFront was created");
+  ok(front.getAnimationPlayersForNode, "The getAnimationPlayersForNode method exists");
+
+  let didThrow = false;
+  try {
+    yield front.getAnimationPlayersForNode(null);
+  } catch (e) {
+    didThrow = true;
+  }
+  ok(didThrow, "An exception was thrown for a missing NodeActor");
+
+  let invalidNode = yield walker.querySelector(walker.rootNode, "title");
+  let players = yield front.getAnimationPlayersForNode(invalidNode);
+  ok(Array.isArray(players), "An array of players was returned");
+  is(players.length, 0, "0 players have been returned for the invalid node");
+
+  yield closeDebuggerClient(client);
+  gBrowser.removeCurrentTab();
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/browser_animation_actors_02.js
@@ -0,0 +1,62 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check the output of getAnimationPlayersForNode
+
+const {AnimationsFront} = require("devtools/server/actors/animation");
+const {InspectorFront} = require("devtools/server/actors/inspector");
+
+add_task(function*() {
+  let doc = yield addTab(MAIN_DOMAIN + "animation.html");
+
+  initDebuggerServer();
+  let client = new DebuggerClient(DebuggerServer.connectPipe());
+  let form = yield connectDebuggerClient(client);
+  let inspector = InspectorFront(client, form);
+  let walker = yield inspector.getWalker();
+  let front = AnimationsFront(client, form);
+
+  yield theRightNumberOfPlayersIsReturned(walker, front);
+  yield playersCanBePausedAndResumed(walker, front);
+
+  yield closeDebuggerClient(client);
+  gBrowser.removeCurrentTab();
+});
+
+function* theRightNumberOfPlayersIsReturned(walker, front) {
+  let node = yield walker.querySelector(walker.rootNode, ".not-animated");
+  let players = yield front.getAnimationPlayersForNode(node);
+  is(players.length, 0, "0 players were returned for the unanimated node");
+
+  node = yield walker.querySelector(walker.rootNode, ".simple-animation");
+  players = yield front.getAnimationPlayersForNode(node);
+  is(players.length, 1, "One animation player was returned");
+
+  node = yield walker.querySelector(walker.rootNode, ".multiple-animations");
+  players = yield front.getAnimationPlayersForNode(node);
+  is(players.length, 2, "Two animation players were returned");
+
+  node = yield walker.querySelector(walker.rootNode, ".transition");
+  players = yield front.getAnimationPlayersForNode(node);
+  is(players.length, 1, "One animation player was returned for the transitioned node");
+}
+
+function* playersCanBePausedAndResumed(walker, front) {
+  let node = yield walker.querySelector(walker.rootNode, ".simple-animation");
+  let [player] = yield front.getAnimationPlayersForNode(node);
+
+  ok(player.initialState, "The player has an initialState");
+  ok(player.getCurrentState, "The player has the getCurrentState method");
+  is(player.initialState.playState, "running", "The animation is currently running");
+
+  yield player.pause();
+  let state = yield player.getCurrentState();
+  is(state.playState, "paused", "The animation is now paused");
+
+  yield player.play();
+  state = yield player.getCurrentState();
+  is(state.playState, "running", "The animation is now running again");
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/browser_animation_actors_03.js
@@ -0,0 +1,79 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check the animation player's initial state
+
+const {AnimationsFront} = require("devtools/server/actors/animation");
+const {InspectorFront} = require("devtools/server/actors/inspector");
+
+add_task(function*() {
+  let doc = yield addTab(MAIN_DOMAIN + "animation.html");
+
+  initDebuggerServer();
+  let client = new DebuggerClient(DebuggerServer.connectPipe());
+  let form = yield connectDebuggerClient(client);
+  let inspector = InspectorFront(client, form);
+  let walker = yield inspector.getWalker();
+  let front = AnimationsFront(client, form);
+
+  yield playerHasAnInitialState(walker, front);
+  yield playerStateIsCorrect(walker, front);
+
+  yield closeDebuggerClient(client);
+  gBrowser.removeCurrentTab();
+});
+
+function* playerHasAnInitialState(walker, front) {
+  let node = yield walker.querySelector(walker.rootNode, ".simple-animation");
+  let [player] = yield front.getAnimationPlayersForNode(node);
+
+  ok(player.initialState, "The player front has an initial state");
+  ok("startTime" in player.initialState, "Player's state has startTime");
+  ok("currentTime" in player.initialState, "Player's state has currentTime");
+  ok("playState" in player.initialState, "Player's state has playState");
+  ok("name" in player.initialState, "Player's state has name");
+  ok("duration" in player.initialState, "Player's state has duration");
+  ok("iterationCount" in player.initialState, "Player's state has iterationCount");
+  ok("isRunningOnCompositor" in player.initialState, "Player's state has isRunningOnCompositor");
+}
+
+function* playerStateIsCorrect(walker, front) {
+  info("Checking the state of the simple animation");
+
+  let node = yield walker.querySelector(walker.rootNode, ".simple-animation");
+  let [player] = yield front.getAnimationPlayersForNode(node);
+  let state = player.initialState;
+
+  is(state.name, "move", "Name is correct");
+  is(state.duration, 2000, "Duration is correct");
+  // null = infinite count
+  is(state.iterationCount, null, "Iteration count is correct");
+  is(state.playState, "running", "PlayState is correct");
+
+  info("Checking the state of the transition");
+
+  node = yield walker.querySelector(walker.rootNode, ".transition");
+  [player] = yield front.getAnimationPlayersForNode(node);
+  state = player.initialState;
+
+  is(state.name, "", "Transition has no name");
+  is(state.duration, 5000, "Transition duration is correct");
+  // transitions run only once
+  is(state.iterationCount, 1, "Transition iteration count is correct");
+  is(state.playState, "running", "Transition playState is correct");
+
+  info("Checking the state of one of multiple animations on a node");
+
+  node = yield walker.querySelector(walker.rootNode, ".multiple-animations");
+  // Checking the 2nd player
+  [, player] = yield front.getAnimationPlayersForNode(node);
+  state = player.initialState;
+
+  is(state.name, "glow", "The 2nd animation's name is correct");
+  is(state.duration, 1000, "The 2nd animation's duration is correct");
+  is(state.iterationCount, 5, "The 2nd animation's iteration count is correct");
+  is(state.playState, "running", "The 2nd animation's playState is correct");
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/browser_animation_actors_04.js
@@ -0,0 +1,58 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check the animation player's updated state
+
+const {AnimationsFront} = require("devtools/server/actors/animation");
+const {InspectorFront} = require("devtools/server/actors/inspector");
+
+add_task(function*() {
+  let doc = yield addTab(MAIN_DOMAIN + "animation.html");
+
+  initDebuggerServer();
+  let client = new DebuggerClient(DebuggerServer.connectPipe());
+  let form = yield connectDebuggerClient(client);
+  let inspector = InspectorFront(client, form);
+  let walker = yield inspector.getWalker();
+  let front = AnimationsFront(client, form);
+
+  yield playStateIsUpdatedDynamically(walker, front);
+
+  yield closeDebuggerClient(client);
+  gBrowser.removeCurrentTab();
+});
+
+function* playStateIsUpdatedDynamically(walker, front) {
+  let node = yield walker.querySelector(walker.rootNode, ".short-animation");
+
+  // Restart the animation to make sure we can get the player (it might already
+  // be finished by now). Do this by toggling the class and forcing a sync reflow
+  // using the CPOW.
+  let cpow = content.document.querySelector(".short-animation");
+  cpow.classList.remove("short-animation");
+  let reflow = cpow.offsetWidth;
+  cpow.classList.add("short-animation");
+
+  let [player] = yield front.getAnimationPlayersForNode(node);
+
+  is(player.initialState.playState, "running",
+    "The playState is running while the transition is running");
+
+  info("Wait until the animation stops (more than 1000ms)");
+  yield wait(1500); // Waiting 1.5sec for good measure
+
+  let state = yield player.getCurrentState();
+  is(state.playState, "finished",
+    "The animation has ended and the state has been updated");
+  ok(state.currentTime > player.initialState.currentTime,
+    "The currentTime has been updated");
+}
+
+function wait(ms) {
+  return new Promise(resolve => {
+    setTimeout(resolve, ms);
+  });
+}
--- a/toolkit/devtools/server/tests/browser/browser_timeline.js
+++ b/toolkit/devtools/server/tests/browser/browser_timeline.js
@@ -4,17 +4,17 @@
 
 "use strict";
 
 // Test that the timeline front's start/stop/isRecording methods work in a
 // simple use case, and that markers events are sent when operations occur.
 
 const {TimelineFront} = require("devtools/server/actors/timeline");
 
-let test = asyncTest(function*() {
+add_task(function*() {
   let doc = yield addTab("data:text/html;charset=utf-8,mop");
 
   initDebuggerServer();
   let client = new DebuggerClient(DebuggerServer.connectPipe());
 
   let form = yield connectDebuggerClient(client);
   let front = TimelineFront(client, form);
 
--- a/toolkit/devtools/server/tests/browser/browser_timeline_actors.js
+++ b/toolkit/devtools/server/tests/browser/browser_timeline_actors.js
@@ -4,17 +4,17 @@
 
 "use strict";
 
 // Test that the timeline can also record data from the memory and framerate
 // actors, emitted as events in tadem with the markers.
 
 const {TimelineFront} = require("devtools/server/actors/timeline");
 
-let test = asyncTest(function*() {
+add_task(function*() {
   let doc = yield addTab("data:text/html;charset=utf-8,mop");
 
   initDebuggerServer();
   let client = new DebuggerClient(DebuggerServer.connectPipe());
   let form = yield connectDebuggerClient(client);
   let front = TimelineFront(client, form);
 
   info("Start timeline marker recording");
--- a/toolkit/devtools/server/tests/browser/browser_timeline_iframes.js
+++ b/toolkit/devtools/server/tests/browser/browser_timeline_iframes.js
@@ -4,17 +4,17 @@
 
 "use strict";
 
 // Test the timeline front receives markers events for operations that occur in
 // iframes.
 
 const {TimelineFront} = require("devtools/server/actors/timeline");
 
-let test = asyncTest(function*() {
+add_task(function*() {
   let doc = yield addTab(MAIN_DOMAIN + "timeline-iframe-parent.html");
 
   initDebuggerServer();
   let client = new DebuggerClient(DebuggerServer.connectPipe());
   let form = yield connectDebuggerClient(client);
   let front = TimelineFront(client, form);
 
   info("Start timeline marker recording");
--- a/toolkit/devtools/server/tests/browser/head.js
+++ b/toolkit/devtools/server/tests/browser/head.js
@@ -16,23 +16,16 @@ const PATH = "browser/toolkit/devtools/s
 const MAIN_DOMAIN = "http://test1.example.org/" + PATH;
 const ALT_DOMAIN = "http://sectest1.example.org/" + PATH;
 const ALT_DOMAIN_SECURED = "https://sectest1.example.org:443/" + PATH;
 
 // All tests are asynchronous.
 waitForExplicitFinish();
 
 /**
- * Define an async test based on a generator function.
- */
-function asyncTest(generator) {
-  return () => Task.spawn(generator).then(null, ok.bind(null, false)).then(finish);
-}
-
-/**
  * Add a new test tab in the browser and load the given url.
  * @param {String} url The url to be loaded in the new tab
  * @return a promise that resolves to the document when the url is loaded
  */
 let addTab = Task.async(function* (url) {
   info("Adding a new tab with URL: '" + url + "'");
   let tab = gBrowser.selectedTab = gBrowser.addTab();
   let loaded = once(gBrowser.selectedBrowser, "load", true);
--- a/toolkit/devtools/server/tests/unit/test_dbgglobal.js
+++ b/toolkit/devtools/server/tests/unit/test_dbgglobal.js
@@ -4,36 +4,36 @@
 Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
 Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
 
 function run_test()
 {
   // Should get an exception if we try to interact with DebuggerServer
   // before we initialize it...
   check_except(function() {
-    DebuggerServer.openListener(-1);
+    DebuggerServer.createListener();
   });
   check_except(DebuggerServer.closeAllListeners);
   check_except(DebuggerServer.connectPipe);
 
   // Allow incoming connections.
   DebuggerServer.init();
 
   // These should still fail because we haven't added a createRootActor
   // implementation yet.
   check_except(function() {
-    DebuggerServer.openListener(-1);
+    DebuggerServer.createListener();
   });
   check_except(DebuggerServer.closeAllListeners);
   check_except(DebuggerServer.connectPipe);
 
   DebuggerServer.registerModule("xpcshell-test/testactors");
 
   // Now they should work.
-  DebuggerServer.openListener(-1);
+  DebuggerServer.createListener();
   DebuggerServer.closeAllListeners();
 
   // Make sure we got the test's root actor all set up.
   let client1 = DebuggerServer.connectPipe();
   client1.hooks = {
     onPacket: function(aPacket1) {
       do_check_eq(aPacket1.from, "root");
       do_check_eq(aPacket1.applicationType, "xpcshell-tests");
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_xpcshell_debugging.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test the xpcshell-test debug support.  Ideally we should have this test
+// next to the xpcshell support code, but that's tricky...
+
+function run_test() {
+  let testFile = do_get_file("xpcshell_debugging_script.js");
+
+  // _setupDebuggerServer is from xpcshell-test's head.js
+  let testResumed = false;
+  let DebuggerServer = _setupDebuggerServer([testFile.path], () => testResumed = true);
+  let transport = DebuggerServer.connectPipe();
+  let client = new DebuggerClient(transport);
+  client.connect(() => {
+    // Even though we have no tabs, listTabs gives us the chromeDebugger.
+    client.listTabs(response => {
+      let chromeDebugger = response.chromeDebugger;
+      client.attachThread(chromeDebugger, (response, threadClient) => {
+        threadClient.addOneTimeListener("paused", (event, packet) => {
+        equal(packet.why.type, "breakpoint",
+              "yay - hit the breakpoint at the first line in our script");
+          // Resume again - next stop should be our "debugger" statement.
+          threadClient.addOneTimeListener("paused", (event, packet) => {
+            equal(packet.why.type, "debuggerStatement",
+                  "yay - hit the 'debugger' statement in our script");
+            threadClient.resume(() => {
+              finishClient(client);
+            });
+          });
+          threadClient.resume();
+        });
+        // tell the thread to do the initial resume.  This would cause the
+        // xpcshell test harness to resume and load the file under test.
+        threadClient.resume(response => {
+          // should have been told to resume the test itself.
+          ok(testResumed);
+          // Now load our test script.
+          load(testFile.path);
+          // and our "paused" listener above should get hit.
+        });
+      });
+    });
+  });
+  do_test_pending();
+}
--- a/toolkit/devtools/server/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/server/tests/unit/xpcshell.ini
@@ -221,8 +221,10 @@ reason = bug 937197
 [test_protocolSpec.js]
 [test_registerClient.js]
 [test_client_request.js]
 [test_monitor_actor.js]
 [test_symbols-01.js]
 [test_symbols-02.js]
 [test_get-executable-lines.js]
 [test_get-executable-lines-source-map.js]
+[test_xpcshell_debugging.js]
+support-files = xpcshell_debugging_script.js
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/xpcshell_debugging_script.js
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This is a file that test_xpcshell_debugging.js debugs.
+
+// We should hit this dump as it is the first debuggable line
+dump("hello from the debugee!\n")
+
+debugger; // and why not check we hit this!?
--- a/toolkit/devtools/transport/tests/unit/head_dbg.js
+++ b/toolkit/devtools/transport/tests/unit/head_dbg.js
@@ -7,16 +7,17 @@ const Ci = Components.interfaces;
 const Cu = Components.utils;
 const Cr = Components.results;
 const CC = Components.Constructor;
 
 const { devtools } =
   Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 const { Promise: promise } =
   Cu.import("resource://gre/modules/Promise.jsm", {});
+const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
 
 const Services = devtools.require("Services");
 const DevToolsUtils = devtools.require("devtools/toolkit/DevToolsUtils.js");
 
 // We do not want to log packets by default, because in some tests,
 // we can be sending large amounts of data. The test harness has
 // trouble dealing with logging all the data, and we end up with
 // intermittent time outs (e.g. bug 775924).
@@ -253,28 +254,30 @@ function writeTestTempFile(aFileName, aC
     } while (aContent.length > 0);
   } finally {
     stream.close();
   }
 }
 
 /*** Transport Factories ***/
 
-function socket_transport() {
+let socket_transport = Task.async(function*() {
   if (!DebuggerServer.listeningSockets) {
-    let listener = DebuggerServer.openListener(-1);
+    let listener = DebuggerServer.createListener();
+    listener.portOrPath = -1 /* any available port */;
     listener.allowConnection = () => true;
+    yield listener.open();
   }
   let port = DebuggerServer._listeners[0].port;
   do_print("Debugger server port is " + port);
-  return DebuggerClient.socketConnect("127.0.0.1", port);
-}
+  return DebuggerClient.socketConnect({ host: "127.0.0.1", port });
+});
 
 function local_transport() {
-  return DebuggerServer.connectPipe();
+  return promise.resolve(DebuggerServer.connectPipe());
 }
 
 /*** Sample Data ***/
 
 let gReallyLong;
 function really_long() {
   if (gReallyLong) {
     return gReallyLong;
--- a/toolkit/devtools/transport/tests/unit/test_bulk_error.js
+++ b/toolkit/devtools/transport/tests/unit/test_bulk_error.js
@@ -46,33 +46,33 @@ TestBulkActor.prototype.requestTypes = {
 };
 
 function add_test_bulk_actor() {
   DebuggerServer.addGlobalActor(TestBulkActor);
 }
 
 /*** Tests ***/
 
-function test_string_error(transportFactory, onReady) {
+let test_string_error = Task.async(function*(transportFactory, onReady) {
   let deferred = promise.defer();
-  let transport = transportFactory();
+  let transport = yield transportFactory();
 
   let client = new DebuggerClient(transport);
   client.connect((app, traits) => {
     do_check_eq(traits.bulk, true);
     client.listTabs(response => {
       deferred.resolve(onReady(client, response).then(() => {
         client.close();
         transport.close();
       }));
     });
   });
 
   return deferred.promise;
-}
+});
 
 /*** Reply Types ***/
 
 function json_reply(client, response) {
   let reallyLong = really_long();
 
   let request = client.startBulkRequest({
     actor: response.testBulk,
--- a/toolkit/devtools/transport/tests/unit/test_client_server_bulk.js
+++ b/toolkit/devtools/transport/tests/unit/test_client_server_bulk.js
@@ -129,26 +129,26 @@ let replyHandlers = {
     });
     return replyDeferred.promise;
   }
 
 };
 
 /*** Tests ***/
 
-function test_bulk_request_cs(transportFactory, actorType, replyType) {
+let test_bulk_request_cs = Task.async(function*(transportFactory, actorType, replyType) {
   // Ensure test files are not present from a failed run
   cleanup_files();
   writeTestTempFile("bulk-input", really_long());
 
   let clientDeferred = promise.defer();
   let serverDeferred = promise.defer();
   let bulkCopyDeferred = promise.defer();
 
-  let transport = transportFactory();
+  let transport = yield transportFactory();
 
   let client = new DebuggerClient(transport);
   client.connect((app, traits) => {
     do_check_eq(traits.bulk, true);
     client.listTabs(clientDeferred.resolve);
   });
 
   clientDeferred.promise.then(response => {
@@ -181,27 +181,27 @@ function test_bulk_request_cs(transportF
     }
   });
 
   return promise.all([
     clientDeferred.promise,
     bulkCopyDeferred.promise,
     serverDeferred.promise
   ]);
-}
+});
 
-function test_json_request_cs(transportFactory, actorType, replyType) {
+let test_json_request_cs = Task.async(function*(transportFactory, actorType, replyType) {
   // Ensure test files are not present from a failed run
   cleanup_files();
   writeTestTempFile("bulk-input", really_long());
 
   let clientDeferred = promise.defer();
   let serverDeferred = promise.defer();
 
-  let transport = transportFactory();
+  let transport = yield transportFactory();
 
   let client = new DebuggerClient(transport);
   client.connect((app, traits) => {
     do_check_eq(traits.bulk, true);
     client.listTabs(clientDeferred.resolve);
   });
 
   clientDeferred.promise.then(response => {
@@ -222,17 +222,17 @@ function test_json_request_cs(transportF
       serverDeferred.resolve();
     }
   });
 
   return promise.all([
     clientDeferred.promise,
     serverDeferred.promise
   ]);
-}
+});
 
 /*** Test Utils ***/
 
 function verify_files() {
   let reallyLong = really_long();
 
   let inputFile = getTestTempFile("bulk-input");
   let outputFile = getTestTempFile("bulk-output");
--- a/toolkit/devtools/transport/tests/unit/test_dbgsocket.js
+++ b/toolkit/devtools/transport/tests/unit/test_dbgsocket.js
@@ -7,92 +7,97 @@ Cu.import("resource://gre/modules/devtoo
 let gPort;
 let gExtraListener;
 
 function run_test()
 {
   do_print("Starting test at " + new Date().toTimeString());
   initTestDebuggerServer();
 
-  add_test(test_socket_conn);
-  add_test(test_socket_shutdown);
+  add_task(test_socket_conn);
+  add_task(test_socket_shutdown);
   add_test(test_pipe_conn);
 
   run_next_test();
 }
 
-function test_socket_conn()
+function* test_socket_conn()
 {
   do_check_eq(DebuggerServer.listeningSockets, 0);
-  let listener = DebuggerServer.openListener(-1);
+  let listener = DebuggerServer.createListener();
+  do_check_true(listener);
+  listener.portOrPath = -1 /* any available port */;
   listener.allowConnection = () => true;
-  do_check_true(listener);
+  listener.open();
   do_check_eq(DebuggerServer.listeningSockets, 1);
   gPort = DebuggerServer._listeners[0].port;
   do_print("Debugger server port is " + gPort);
   // Open a second, separate listener
-  gExtraListener = DebuggerServer.openListener(-1);
+  gExtraListener = DebuggerServer.createListener();
+  gExtraListener.portOrPath = -1;
   gExtraListener.allowConnection = () => true;
+  gExtraListener.open();
   do_check_eq(DebuggerServer.listeningSockets, 2);
 
   do_print("Starting long and unicode tests at " + new Date().toTimeString());
   let unicodeString = "(╯°□°)╯︵ ┻━┻";
-  let transport = DebuggerClient.socketConnect("127.0.0.1", gPort);
+  let transport = yield DebuggerClient.socketConnect({
+    host: "127.0.0.1",
+    port: gPort
+  });
+  let closedDeferred = promise.defer();
   transport.hooks = {
     onPacket: function(aPacket) {
       this.onPacket = function(aPacket) {
         do_check_eq(aPacket.unicode, unicodeString);
         transport.close();
       }
       // Verify that things work correctly when bigger than the output
       // transport buffers and when transporting unicode...
       transport.send({to: "root",
                       type: "echo",
                       reallylong: really_long(),
                       unicode: unicodeString});
       do_check_eq(aPacket.from, "root");
     },
     onClosed: function(aStatus) {
-      run_next_test();
+      closedDeferred.resolve();
     },
   };
   transport.ready();
+  return closedDeferred.promise;
 }
 
-function test_socket_shutdown()
+function* test_socket_shutdown()
 {
   do_check_eq(DebuggerServer.listeningSockets, 2);
   gExtraListener.close();
   do_check_eq(DebuggerServer.listeningSockets, 1);
   do_check_true(DebuggerServer.closeAllListeners());
   do_check_eq(DebuggerServer.listeningSockets, 0);
   // Make sure closing the listener twice does nothing.
   do_check_false(DebuggerServer.closeAllListeners());
   do_check_eq(DebuggerServer.listeningSockets, 0);
 
   do_print("Connecting to a server socket at " + new Date().toTimeString());
-  let transport = DebuggerClient.socketConnect("127.0.0.1", gPort);
-  transport.hooks = {
-    onPacket: function(aPacket) {
-      // Shouldn't reach this, should never connect.
-      do_check_true(false);
-    },
+  try {
+    let transport = yield DebuggerClient.socketConnect({
+      host: "127.0.0.1",
+      port: gPort
+    });
+  } catch(e if e.result == Cr.NS_ERROR_CONNECTION_REFUSED ||
+               e.result == Cr.NS_ERROR_NET_TIMEOUT) {
+    // The connection should be refused here, but on slow or overloaded
+    // machines it may just time out.
+    do_check_true(true);
+    return;
+  }
 
-    onClosed: function(aStatus) {
-      do_print("test_socket_shutdown onClosed called at " + new Date().toTimeString());
-      // The connection should be refused here, but on slow or overloaded
-      // machines it may just time out.
-      let expected = [ Cr.NS_ERROR_CONNECTION_REFUSED, Cr.NS_ERROR_NET_TIMEOUT ];
-      do_check_neq(expected.indexOf(aStatus), -1);
-      run_next_test();
-    }
-  };
-
-  do_print("Initializing input stream at " + new Date().toTimeString());
-  transport.ready();
+  // Shouldn't reach this, should never connect.
+  do_check_true(false);
 }
 
 function test_pipe_conn()
 {
   let transport = DebuggerServer.connectPipe();
   transport.hooks = {
     onPacket: function(aPacket) {
       do_check_eq(aPacket.from, "root");
--- a/toolkit/devtools/transport/tests/unit/test_dbgsocket_connection_drop.js
+++ b/toolkit/devtools/transport/tests/unit/test_dbgsocket_connection_drop.js
@@ -12,20 +12,20 @@ Cu.import("resource://gre/modules/devtoo
 Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
 
 const { RawPacket } = devtools.require("devtools/toolkit/transport/packets");
 
 function run_test() {
   do_print("Starting test at " + new Date().toTimeString());
   initTestDebuggerServer();
 
-  add_test(test_socket_conn_drops_after_invalid_header);
-  add_test(test_socket_conn_drops_after_invalid_header_2);
-  add_test(test_socket_conn_drops_after_too_large_length);
-  add_test(test_socket_conn_drops_after_too_long_header);
+  add_task(test_socket_conn_drops_after_invalid_header);
+  add_task(test_socket_conn_drops_after_invalid_header_2);
+  add_task(test_socket_conn_drops_after_too_large_length);
+  add_task(test_socket_conn_drops_after_too_long_header);
   run_next_test();
 }
 
 function test_socket_conn_drops_after_invalid_header() {
   return test_helper('fluff30:27:{"to":"root","type":"echo"}');
 }
 
 function test_socket_conn_drops_after_invalid_header_2() {
@@ -41,31 +41,38 @@ function test_socket_conn_drops_after_to
   // The packet header is currently limited to no more than 200 bytes
   let rawPacket = '4305724038957487634549823475894325';
   for (let i = 0; i < 8; i++) {
     rawPacket += rawPacket;
   }
   return test_helper(rawPacket + ':');
 }
 
-function test_helper(payload) {
-  let listener = DebuggerServer.openListener(-1);
+let test_helper = Task.async(function*(payload) {
+  let listener = DebuggerServer.createListener();
+  listener.portOrPath = -1;
   listener.allowConnection = () => true;
+  listener.open();
 
-  let transport = DebuggerClient.socketConnect("127.0.0.1", listener.port);
+  let transport = yield DebuggerClient.socketConnect({
+    host: "127.0.0.1",
+    port: listener.port
+  });
+  let closedDeferred = promise.defer();
   transport.hooks = {
     onPacket: function(aPacket) {
       this.onPacket = function(aPacket) {
         do_throw(new Error("This connection should be dropped."));
         transport.close();
       };
 
       // Inject the payload directly into the stream.
       transport._outgoing.push(new RawPacket(transport, payload));
       transport._flushOutgoing();
     },
     onClosed: function(aStatus) {
       do_check_true(true);
-      run_next_test();
+      closedDeferred.resolve();
     },
   };
   transport.ready();
-}
+  return closedDeferred.promise;
+});
--- a/toolkit/devtools/transport/tests/unit/test_no_bulk.js
+++ b/toolkit/devtools/transport/tests/unit/test_no_bulk.js
@@ -21,28 +21,28 @@ function run_test() {
     DebuggerServer.destroy();
   });
 
   run_next_test();
 }
 
 /*** Tests ***/
 
-function test_bulk_send_error(transportFactory) {
+let test_bulk_send_error = Task.async(function*(transportFactory) {
   let deferred = promise.defer();
-  let transport = transportFactory();
+  let transport = yield transportFactory();
 
   let client = new DebuggerClient(transport);
   client.connect((app, traits) => {
     do_check_false(traits.bulk);
 
     try {
       client.startBulkRequest();
       do_throw(new Error("Can't use bulk since server doesn't support it"));
     } catch(e) {
       do_check_true(true);
     }
 
     deferred.resolve();
   });
 
   return deferred.promise;
-}
+});
--- a/toolkit/devtools/transport/tests/unit/test_queue.js
+++ b/toolkit/devtools/transport/tests/unit/test_queue.js
@@ -20,28 +20,28 @@ function run_test() {
     DebuggerServer.destroy();
   });
 
   run_next_test();
 }
 
 /*** Tests ***/
 
-function test_transport(transportFactory) {
+let test_transport = Task.async(function*(transportFactory) {
   let clientDeferred = promise.defer();
   let serverDeferred = promise.defer();
 
   // Ensure test files are not present from a failed run
   cleanup_files();
   let reallyLong = really_long();
   writeTestTempFile("bulk-input", reallyLong);
 
   do_check_eq(Object.keys(DebuggerServer._connections).length, 0);
 
-  let transport = transportFactory();
+  let transport = yield transportFactory();
 
   // Sending from client to server
   function write_data({copyFrom}) {
     NetUtil.asyncFetch(getTestTempFile("bulk-input"), function(input, status) {
       copyFrom(input).then(() => {
         input.close();
       });
     });
@@ -128,17 +128,17 @@ function test_transport(transportFactory
     onClosed: function() {
       do_throw("Transport closed before we expected");
     }
   };
 
   transport.ready();
 
   return promise.all([clientDeferred.promise, serverDeferred.promise]);
-}
+});
 
 /*** Test Utils ***/
 
 function verify() {
   let reallyLong = really_long();
 
   let inputFile = getTestTempFile("bulk-input");
   let outputFile = getTestTempFile("bulk-output");
--- a/toolkit/devtools/transport/tests/unit/test_transport_bulk.js
+++ b/toolkit/devtools/transport/tests/unit/test_transport_bulk.js
@@ -18,30 +18,30 @@ function run_test() {
   run_next_test();
 }
 
 /*** Tests ***/
 
 /**
  * This tests a one-way bulk transfer at the transport layer.
  */
-function test_bulk_transfer_transport(transportFactory) {
+let test_bulk_transfer_transport = Task.async(function*(transportFactory) {
   do_print("Starting bulk transfer test at " + new Date().toTimeString());
 
   let clientDeferred = promise.defer();
   let serverDeferred = promise.defer();
 
   // Ensure test files are not present from a failed run
   cleanup_files();
   let reallyLong = really_long();
   writeTestTempFile("bulk-input", reallyLong);
 
   do_check_eq(Object.keys(DebuggerServer._connections).length, 0);
 
-  let transport = transportFactory();
+  let transport = yield transportFactory();
 
   // Sending from client to server
   function write_data({copyFrom}) {
     NetUtil.asyncFetch(getTestTempFile("bulk-input"), function(input, status) {
       copyFrom(input).then(() => {
         input.close();
       });
     });
@@ -99,17 +99,17 @@ function test_bulk_transfer_transport(tr
     onClosed: function() {
       do_throw("Transport closed before we expected");
     }
   };
 
   transport.ready();
 
   return promise.all([clientDeferred.promise, serverDeferred.promise]);
-}
+});
 
 /*** Test Utils ***/
 
 function verify() {
   let reallyLong = really_long();
 
   let inputFile = getTestTempFile("bulk-input");
   let outputFile = getTestTempFile("bulk-output");
--- a/toolkit/modules/Sqlite.jsm
+++ b/toolkit/modules/Sqlite.jsm
@@ -5,38 +5,43 @@
 "use strict";
 
 this.EXPORTED_SYMBOLS = [
   "Sqlite",
 ];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
-Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+// The time to wait before considering a transaction stuck and rejecting it.
+const TRANSACTIONS_QUEUE_TIMEOUT_MS = 120000 // 2 minutes
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
                                   "resource://gre/modules/AsyncShutdown.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Promise",
-                                  "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Log",
                                   "resource://gre/modules/Log.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
                                   "resource://services-common/utils.js");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyServiceGetter(this, "FinalizationWitnessService",
                                    "@mozilla.org/toolkit/finalizationwitness;1",
                                    "nsIFinalizationWitnessService");
-
+XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
+                                  "resource://gre/modules/PromiseUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "console",
+                                  "resource://gre/modules/devtools/Console.jsm");
 
 // Counts the number of created connections per database basename(). This is
 // used for logging to distinguish connection instances.
 let connectionCounters = new Map();
 
 // Tracks identifiers of wrapped connections, that are Storage connections
 // opened through mozStorage and then wrapped by Sqlite.jsm to use its syntactic
 // sugar API.  Since these connections have an unknown origin, we use this set
@@ -186,17 +191,17 @@ XPCOMUtils.defineLazyGetter(this, "Barri
  * OpenedConnection. When the witness detects a garbage collection,
  * this object can be used to close the connection.
  *
  * This object contains more methods than just `close`.  When
  * OpenedConnection needs to use the methods in this object, it will
  * dispatch its method calls here.
  */
 function ConnectionData(connection, identifier, options={}) {
-  this._log = Log.repository.getLoggerWithMessagePrefix("Sqlite.Connection." +
+  this._log = Log.repository.getLoggerWithMessagePrefix("Sqlite.Connection",
                                                         identifier + ": ");
   this._log.info("Opened");
 
   this._dbConn = connection;
 
   // This is a unique identifier for the connection, generated through
   // getIdentifierByPath.  It may be used for logging or as a key in Maps.
   this._identifier = identifier;
@@ -209,37 +214,42 @@ function ConnectionData(connection, iden
 
   // A map from statement index to mozIStoragePendingStatement, to allow for
   // canceling prior to finalizing the mozIStorageStatements.
   this._pendingStatements = new Map();
 
   // Increments for each executed statement for the life of the connection.
   this._statementCounter = 0;
 
-  this._inProgressTransaction = null;
+  this._hasInProgressTransaction = false;
+  // Manages a chain of transactions promises, so that new transactions
+  // always happen in queue to the previous ones.  It never rejects.
+  this._transactionQueue = Promise.resolve();
 
   this._idleShrinkMS = options.shrinkMemoryOnConnectionIdleMS;
   if (this._idleShrinkMS) {
     this._idleShrinkTimer = Cc["@mozilla.org/timer;1"]
                               .createInstance(Ci.nsITimer);
     // We wait for the first statement execute to start the timer because
     // shrinking now would not do anything.
   }
 
-  this._deferredClose = Promise.defer();
+  // Deferred whose promise is resolved when the connection closing procedure
+  // is complete.
+  this._deferredClose = PromiseUtils.defer();
   this._closeRequested = false;
 
   Barriers.connections.client.addBlocker(
     this._identifier + ": waiting for shutdown",
     this._deferredClose.promise,
     () =>  ({
       identifier: this._identifier,
       isCloseRequested: this._closeRequested,
       hasDbConn: !!this._dbConn,
-      hasInProgressTransaction: !!this._inProgressTransaction,
+      hasInProgressTransaction: this._hasInProgressTransaction,
       pendingStatements: this._pendingStatements.size,
       statementCounter: this._statementCounter,
     })
   );
 }
 
 /**
  * Map of connection identifiers to ConnectionData objects
@@ -263,33 +273,26 @@ ConnectionData.prototype = Object.freeze
 
     this._log.debug("Request to close connection.");
     this._clearIdleShrinkTimer();
 
     // We need to take extra care with transactions during shutdown.
     //
     // If we don't have a transaction in progress, we can proceed with shutdown
     // immediately.
-    if (!this._inProgressTransaction) {
-      this._finalize(this._deferredClose);
-      return this._deferredClose.promise;
+    if (!this._hasInProgressTransaction) {
+      return this._finalize();
     }
 
-    // Else if we do have a transaction in progress, we forcefully roll it
-    // back. This is an async task, so we wait on it to finish before
-    // performing finalization.
+    // If instead we do have a transaction in progress, it might be rollback-ed
+    // automaticall by closing the connection.  Regardless, we wait for its
+    // completion, next enqueued transactions will be rejected.
     this._log.warn("Transaction in progress at time of close. Rolling back.");
 
-    let onRollback = this._finalize.bind(this, this._deferredClose);
-
-    this.execute("ROLLBACK TRANSACTION").then(onRollback, onRollback);
-    this._inProgressTransaction.reject(new Error("Connection being closed."));
-    this._inProgressTransaction = null;
-
-    return this._deferredClose.promise;
+    return this._transactionQueue.then(() => this._finalize());
   },
 
   clone: function (readOnly=false) {
     this.ensureOpen();
 
     this._log.debug("Request to clone connection.");
 
     let options = {
@@ -297,17 +300,17 @@ ConnectionData.prototype = Object.freeze
       readOnly: readOnly,
     };
     if (this._idleShrinkMS)
       options.shrinkMemoryOnConnectionIdleMS = this._idleShrinkMS;
 
     return cloneStorageConnection(options);
   },
 
-  _finalize: function (deferred) {
+  _finalize: function () {
     this._log.debug("Finalizing connection.");
     // Cancel any pending statements.
     for (let [k, statement] of this._pendingStatements) {
       statement.cancel();
     }
     this._pendingStatements.clear();
 
     // We no longer need to track these.
@@ -330,26 +333,27 @@ ConnectionData.prototype = Object.freeze
 
     // We must always close the connection at the Sqlite.jsm-level, not
     // necessarily at the mozStorage-level.
     let markAsClosed = () => {
       this._log.info("Closed");
       this._dbConn = null;
       // Now that the connection is closed, no need to keep
       // a blocker for Barriers.connections.
-      Barriers.connections.client.removeBlocker(deferred.promise);
-      deferred.resolve();
+      Barriers.connections.client.removeBlocker(this._deferredClose.promise);
+      this._deferredClose.resolve();
     }
     if (wrappedConnections.has(this._identifier)) {
       wrappedConnections.delete(this._identifier);
       markAsClosed();
     } else {
       this._log.debug("Calling asyncClose().");
       this._dbConn.asyncClose(markAsClosed);
     }
+    return this._deferredClose.promise;
   },
 
   executeCached: function (sql, params=null, onRow=null) {
     this.ensureOpen();
 
     if (!sql) {
       throw new Error("sql argument is empty.");
     }
@@ -357,35 +361,33 @@ ConnectionData.prototype = Object.freeze
     let statement = this._cachedStatements.get(sql);
     if (!statement) {
       statement = this._dbConn.createAsyncStatement(sql);
       this._cachedStatements.set(sql, statement);
     }
 
     this._clearIdleShrinkTimer();
 
-    let deferred = Promise.defer();
-
-    try {
-      this._executeStatement(sql, statement, params, onRow).then(
-        result => {
-          this._startIdleShrinkTimer();
-          deferred.resolve(result);
-        },
-        error => {
-          this._startIdleShrinkTimer();
-          deferred.reject(error);
-        }
-      );
-    } catch (ex) {
-      this._startIdleShrinkTimer();
-      throw ex;
-    }
-
-    return deferred.promise;
+    return new Promise((resolve, reject) => {
+      try {
+        this._executeStatement(sql, statement, params, onRow).then(
+          result => {
+            this._startIdleShrinkTimer();
+            resolve(result);
+          },
+          error => {
+            this._startIdleShrinkTimer();
+            reject(error);
+          }
+        );
+      } catch (ex) {
+        this._startIdleShrinkTimer();
+        throw ex;
+      }
+    });
   },
 
   execute: function (sql, params=null, onRow=null) {
     if (typeof(sql) != "string") {
       throw new Error("Must define SQL to execute as a string: " + sql);
     }
 
     this.ensureOpen();
@@ -397,113 +399,142 @@ ConnectionData.prototype = Object.freeze
     this._clearIdleShrinkTimer();
 
     let onFinished = () => {
       this._anonymousStatements.delete(index);
       statement.finalize();
       this._startIdleShrinkTimer();
     };
 
-    let deferred = Promise.defer();
-
-    try {
-      this._executeStatement(sql, statement, params, onRow).then(
-        rows => {
-          onFinished();
-          deferred.resolve(rows);
-        },
-        error => {
-          onFinished();
-          deferred.reject(error);
-        }
-      );
-    } catch (ex) {
-      onFinished();
-      throw ex;
-    }
-
-    return deferred.promise;
+    return new Promise((resolve, reject) => {
+      try {
+        this._executeStatement(sql, statement, params, onRow).then(
+          rows => {
+            onFinished();
+            resolve(rows);
+          },
+          error => {
+            onFinished();
+            reject(error);
+          }
+        );
+      } catch (ex) {
+        onFinished();
+        throw ex;
+      }
+    });
   },
 
   get transactionInProgress() {
-    return this._open && !!this._inProgressTransaction;
+    return this._open && this._hasInProgressTransaction;
   },
 
   executeTransaction: function (func, type) {
     this.ensureOpen();
 
-    if (this._inProgressTransaction) {
-      throw new Error("A transaction is already active. Only one transaction " +
-                      "can be active at a time.");
-    }
-
     this._log.debug("Beginning transaction");
-    let deferred = Promise.defer();
-    this._inProgressTransaction = deferred;
-    Task.spawn(function doTransaction() {
-      // It's tempting to not yield here and rely on the implicit serial
-      // execution of issued statements. However, the yield serves an important
-      // purpose: catching errors in statement execution.
-      yield this.execute("BEGIN " + type + " TRANSACTION");
 
-      let result;
-      try {
-        result = yield Task.spawn(func);
-      } catch (ex) {
-        // It's possible that a request to close the connection caused the
-        // error.
-        // Assertion: close() will unset
-        // this._inProgressTransaction when called.
-        if (!this._inProgressTransaction) {
-          this._log.warn("Connection was closed while performing transaction. " +
-                         "Received error should be due to closed connection: " +
-                         CommonUtils.exceptionStr(ex));
-          throw ex;
-        }
-
-        this._log.warn("Error during transaction. Rolling back: " +
-                       CommonUtils.exceptionStr(ex));
-        try {
-          yield this.execute("ROLLBACK TRANSACTION");
-        } catch (inner) {
-          this._log.warn("Could not roll back transaction. This is weird: " +
-                         CommonUtils.exceptionStr(inner));
-        }
-
-        throw ex;
+    let promise = this._transactionQueue.then(() => {
+      if (this._closeRequested) {
+        throw new Error("Transaction canceled due to a closed connection.");
       }
 
-      // See comment above about connection being closed during transaction.
-      if (!this._inProgressTransaction) {
-        this._log.warn("Connection was closed while performing transaction. " +
-                       "Unable to commit.");
-        throw new Error("Connection closed before transaction committed.");
-      }
+      let transactionPromise = Task.spawn(function* () {
+        // At this point we should never have an in progress transaction, since
+        // they are enqueued.
+        if (this._hasInProgressTransaction) {
+          console.error("Unexpected transaction in progress when trying to start a new one.");
+        }
+        this._hasInProgressTransaction = true;
+        try {
+          // We catch errors in statement execution to detect nested transactions.
+          try {
+            yield this.execute("BEGIN " + type + " TRANSACTION");
+          } catch (ex) {
+            // Unfortunately, if we are wrapping an existing connection, a
+            // transaction could have been started by a client of the same
+            // connection that doesn't use Sqlite.jsm (e.g. C++ consumer).
+            // The best we can do is proceed without a transaction and hope
+            // things won't break.
+            if (wrappedConnections.has(this._identifier)) {
+              this._log.warn("A new transaction could not be started cause the wrapped connection had one in progress: " +
+                             CommonUtils.exceptionStr(ex));
+              // Unmark the in progress transaction, since it's managed by
+              // some other non-Sqlite.jsm client.  See the comment above.
+              this._hasInProgressTransaction = false;
+            } else {
+              this._log.warn("A transaction was already in progress, likely a nested transaction: " +
+                             CommonUtils.exceptionStr(ex));
+              throw ex;
+            }
+          }
 
-      try {
-        yield this.execute("COMMIT TRANSACTION");
-      } catch (ex) {
-        this._log.warn("Error committing transaction: " +
-                       CommonUtils.exceptionStr(ex));
-        throw ex;
-      }
+          let result;
+          try {
+            result = yield Task.spawn(func);
+          } catch (ex) {
+            // It's possible that the exception has been caused by trying to
+            // close the connection in the middle of a transaction.
+            if (this._closeRequested) {
+              this._log.warn("Connection closed while performing a transaction: " +
+                             CommonUtils.exceptionStr(ex));
+            } else {
+              this._log.warn("Error during transaction. Rolling back: " +
+                             CommonUtils.exceptionStr(ex));
+              // If we began a transaction, we must rollback it.
+              if (this._hasInProgressTransaction) {
+                try {
+                  yield this.execute("ROLLBACK TRANSACTION");
+                } catch (inner) {
+                  this._log.warn("Could not roll back transaction: " +
+                                 CommonUtils.exceptionStr(inner));
+                }
+              }
+            }
+            // Rethrow the exception.
+            throw ex;
+          }
+
+          // See comment above about connection being closed during transaction.
+          if (this._closeRequested) {
+            this._log.warn("Connection closed before committing the transaction.");
+            throw new Error("Connection closed before committing the transaction.");
+          }
 
-      throw new Task.Result(result);
-    }.bind(this)).then(
-      function onSuccess(result) {
-        this._inProgressTransaction = null;
-        deferred.resolve(result);
-      }.bind(this),
-      function onError(error) {
-        this._inProgressTransaction = null;
-        deferred.reject(error);
-      }.bind(this)
-    );
+          // If we began a transaction, we must commit it.
+          if (this._hasInProgressTransaction) {
+            try {
+              yield this.execute("COMMIT TRANSACTION");
+            } catch (ex) {
+              this._log.warn("Error committing transaction: " +
+                             CommonUtils.exceptionStr(ex));
+              throw ex;
+            }
+          }
 
-    return deferred.promise;
+          return result;
+        } finally {
+          this._hasInProgressTransaction = false;
+        }
+      }.bind(this));
+
+      // If a transaction yields on a never resolved promise, or is mistakenly
+      // nested, it could hang the transactions queue forever.  Thus we timeout
+      // the execution after a meaningful amount of time, to ensure in any case
+      // we'll proceed after a while.
+      let timeoutPromise = new Promise((resolve, reject) => {
+        setTimeout(() => reject(new Error("Transaction timeout, most likely caused by unresolved pending work.")),
+                   TRANSACTIONS_QUEUE_TIMEOUT_MS);
+      });
+      return Promise.race([transactionPromise, timeoutPromise]);
+    });
+    // Atomically update the queue before anyone else has a chance to enqueue
+    // further transactions.
+    this._transactionQueue = promise.catch(ex => { console.error(ex) });
+    return promise;
   },
 
   shrinkMemory: function () {
     this._log.info("Shrinking memory usage.");
     let onShrunk = this._clearIdleShrinkTimer.bind(this);
     return this.execute("PRAGMA shrink_memory").then(onShrunk, onShrunk);
   },
 
@@ -570,17 +601,17 @@ ConnectionData.prototype = Object.freeze
     if (onRow && typeof(onRow) != "function") {
       throw new Error("onRow must be a function. Got: " + onRow);
     }
 
     this._bindParameters(statement, params);
 
     let index = this._statementCounter++;
 
-    let deferred = Promise.defer();
+    let deferred = PromiseUtils.defer();
     let userCancelled = false;
     let errors = [];
     let rows = [];
     let handledRow = false;
 
     // Don't incur overhead for serializing params unless the messages go
     // somewhere.
     if (this._log.level <= Log.Level.Trace) {
@@ -733,17 +764,16 @@ function openConnection(options) {
   if (!options.path) {
     throw new Error("path not specified in connection options.");
   }
 
   if (isClosed) {
     throw new Error("Sqlite.jsm has been shutdown. Cannot open connection to: " + options.path);
   }
 
-
   // Retains absolute paths and normalizes relative as relative to profile.
   let path = OS.Path.join(OS.Constants.Path.profileDir, options.path);
 
   let sharedMemoryCache = "sharedMemoryCache" in options ?
                             options.sharedMemoryCache : true;
 
   let openedOptions = {};
 
@@ -756,40 +786,41 @@ function openConnection(options) {
     openedOptions.shrinkMemoryOnConnectionIdleMS =
       options.shrinkMemoryOnConnectionIdleMS;
   }
 
   let file = FileUtils.File(path);
   let identifier = getIdentifierByPath(path);
 
   log.info("Opening database: " + path + " (" + identifier + ")");
-  let deferred = Promise.defer();
-  let dbOptions = null;
-  if (!sharedMemoryCache) {
-    dbOptions = Cc["@mozilla.org/hash-property-bag;1"].
-      createInstance(Ci.nsIWritablePropertyBag);
-    dbOptions.setProperty("shared", false);
-  }
-  Services.storage.openAsyncDatabase(file, dbOptions, function(status, connection) {
-    if (!connection) {
-      log.warn("Could not open connection: " + status);
-      deferred.reject(new Error("Could not open connection: " + status));
-      return;
+
+  return new Promise((resolve, reject) => {
+    let dbOptions = null;
+    if (!sharedMemoryCache) {
+      dbOptions = Cc["@mozilla.org/hash-property-bag;1"].
+        createInstance(Ci.nsIWritablePropertyBag);
+      dbOptions.setProperty("shared", false);
     }
-    log.info("Connection opened");
-    try {
-      deferred.resolve(
-        new OpenedConnection(connection.QueryInterface(Ci.mozIStorageAsyncConnection),
-                            identifier, openedOptions));
-    } catch (ex) {
-      log.warn("Could not open database: " + CommonUtils.exceptionStr(ex));
-      deferred.reject(ex);
-    }
+    Services.storage.openAsyncDatabase(file, dbOptions, (status, connection) => {
+      if (!connection) {
+        log.warn("Could not open connection: " + status);
+        reject(new Error("Could not open connection: " + status));
+        return;
+      }
+      log.info("Connection opened");
+      try {
+        resolve(
+          new OpenedConnection(connection.QueryInterface(Ci.mozIStorageAsyncConnection),
+                              identifier, openedOptions));
+      } catch (ex) {
+        log.warn("Could not open database: " + CommonUtils.exceptionStr(ex));
+        reject(ex);
+      }
+    });
   });
-  return deferred.promise;
 }
 
 /**
  * Creates a clone of an existing and open Storage connection.  The clone has
  * the same underlying characteristics of the original connection and is
  * returned in form of an OpenedConnection handle.
  *
  * The following parameters can control the cloned connection:
@@ -841,33 +872,33 @@ function cloneStorageConnection(options)
     openedOptions.shrinkMemoryOnConnectionIdleMS =
       options.shrinkMemoryOnConnectionIdleMS;
   }
 
   let path = source.databaseFile.path;
   let identifier = getIdentifierByPath(path);
 
   log.info("Cloning database: " + path + " (" + identifier + ")");
-  let deferred = Promise.defer();
 
-  source.asyncClone(!!options.readOnly, (status, connection) => {
-    if (!connection) {
-      log.warn("Could not clone connection: " + status);
-      deferred.reject(new Error("Could not clone connection: " + status));
-    }
-    log.info("Connection cloned");
-    try {
-      let conn = connection.QueryInterface(Ci.mozIStorageAsyncConnection);
-      deferred.resolve(new OpenedConnection(conn, identifier, openedOptions));
-    } catch (ex) {
-      log.warn("Could not clone database: " + CommonUtils.exceptionStr(ex));
-      deferred.reject(ex);
-    }
+  return new Promise((resolve, reject) => {
+    source.asyncClone(!!options.readOnly, (status, connection) => {
+      if (!connection) {
+        log.warn("Could not clone connection: " + status);
+        reject(new Error("Could not clone connection: " + status));
+      }
+      log.info("Connection cloned");
+      try {
+        let conn = connection.QueryInterface(Ci.mozIStorageAsyncConnection);
+        resolve(new OpenedConnection(conn, identifier, openedOptions));
+      } catch (ex) {
+        log.warn("Could not clone database: " + CommonUtils.exceptionStr(ex));
+        reject(ex);
+      }
+    });
   });
-  return deferred.promise;
 }
 
 /**
  * Wraps an existing and open Storage connection with Sqlite.jsm API.  The
  * wrapped connection clone has the same underlying characteristics of the
  * original connection and is returned in form of an OpenedConnection handle.
  *
  * Clients are responsible for closing both the Sqlite.jsm wrapper and the
@@ -1150,16 +1181,31 @@ OpenedConnection.prototype = Object.free
    */
   get transactionInProgress() {
     return this._connectionData.transactionInProgress;
   },
 
   /**
    * Perform a transaction.
    *
+   * *****************************************************************************
+   * YOU SHOULD _NEVER_ NEST executeTransaction CALLS FOR ANY REASON, NOR
+   * DIRECTLY, NOR THROUGH OTHER PROMISES.
+   * FOR EXAMPLE, NEVER DO SOMETHING LIKE:
+   *   yield executeTransaction(function* () {
+   *     ...some_code...
+   *     yield executeTransaction(function* () { // WRONG!
+   *       ...some_code...
+   *     })
+   *     yield someCodeThatExecuteTransaction(); // WRONG!
+   *     yield neverResolvedPromise; // WRONG!
+   *   });
+   * NESTING CALLS WILL BLOCK ANY FUTURE TRANSACTION UNTIL A TIMEOUT KICKS IN.
+   * *****************************************************************************
+   *
    * A transaction is specified by a user-supplied function that is a
    * generator function which can be used by Task.jsm's Task.spawn(). The
    * function receives this connection instance as its argument.
    *
    * The supplied function is expected to yield promises. These are often
    * promises created by calling `execute` and `executeCached`. If the
    * generator is exhausted without any errors being thrown, the
    * transaction is committed. If an error occurs, the transaction is
--- a/toolkit/modules/tests/xpcshell/test_sqlite.js
+++ b/toolkit/modules/tests/xpcshell/test_sqlite.js
@@ -45,77 +45,77 @@ function getConnection(dbName, extraOpti
   let options = {path: path};
   for (let [k, v] in Iterator(extraOptions)) {
     options[k] = v;
   }
 
   return Sqlite.openConnection(options);
 }