Merge fx-team to central, a=merge
authorWes Kocher <wkocher@mozilla.com>
Wed, 28 Oct 2015 16:41:39 -0700
changeset 269962 769f29c92bb2aaf8310720f9a0628fb2896086c5
parent 269930 286091b03549a8ad6a0beaa6aaa72194481ebf1e (current diff)
parent 269961 8486fdc26fa6a41a845ea5efc72953b539988607 (diff)
child 270021 1e700005a0ddf2b17803213e1f3f8d78a7a618b8
push id29595
push userkwierso@gmail.com
push dateWed, 28 Oct 2015 23:41:46 +0000
treeherdermozilla-central@769f29c92bb2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone44.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to central, a=merge
devtools/client/markupview/test/browser_markupview_dragdrop_isDragging.js
devtools/client/markupview/test/browser_markupview_dragdrop_textSelection.js
toolkit/components/workerlz4/lz4.cpp
toolkit/components/workerlz4/lz4.js
toolkit/components/workerlz4/lz4_internal.js
toolkit/components/workerlz4/moz.build
toolkit/components/workerlz4/tests/xpcshell/data/chrome.manifest
toolkit/components/workerlz4/tests/xpcshell/data/compression.lz
toolkit/components/workerlz4/tests/xpcshell/data/worker_lz4.js
toolkit/components/workerlz4/tests/xpcshell/test_lz4.js
toolkit/components/workerlz4/tests/xpcshell/xpcshell.ini
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1561,16 +1561,18 @@ var gBrowserInit = {
     BookmarkingUI.uninit();
 
     TabsInTitlebar.uninit();
 
     ToolbarIconColor.uninit();
 
     TabletModeUpdater.uninit();
 
+    gTabletModePageCounter.finish();
+
     BrowserOnClick.uninit();
 
     DevEdition.uninit();
 
     TrackingProtection.uninit();
 
     gMenuButtonUpdateBadge.uninit();
 
@@ -4438,16 +4440,17 @@ var XULBrowserWindow = {
       }
 
       if (gURLBar) {
         URLBarSetURI(aLocationURI);
 
         BookmarkingUI.onLocationChange();
         SocialUI.updateState(location);
         UITour.onLocationChange(location);
+        gTabletModePageCounter.inc();
       }
 
       // Utility functions for disabling find
       var shouldDisableFind = function shouldDisableFind(aDocument) {
         let docElt = aDocument.documentElement;
         return docElt && docElt.getAttribute("disablefastfind") == "true";
       }
 
@@ -5435,16 +5438,39 @@ var TabletModeUpdater = {
       document.documentElement.setAttribute("tabletmode", "true");
     } else {
       document.documentElement.removeAttribute("tabletmode");
     }
     TabsInTitlebar.updateAppearance(true);
   },
 };
 
+var gTabletModePageCounter = {
+  inc() {
+    if (!AppConstants.isPlatformAndVersionAtLeast("win", "10.0")) {
+      this.inc = () => {};
+      return;
+    }
+    this.inc = this._realInc;
+    this.inc();
+  },
+
+  _desktopCount: 0,
+  _tabletCount: 0,
+  _realInc() {
+    let inTabletMode = document.documentElement.hasAttribute("tabletmode");
+    this[inTabletMode ? "_tabletCount" : "_desktopCount"]++;
+  },
+
+  finish() {
+    Services.telemetry.getKeyedHistogramById("FX_TABLETMODE_PAGE_LOAD").add("tablet", this._tabletCount);
+    Services.telemetry.getKeyedHistogramById("FX_TABLETMODE_PAGE_LOAD").add("desktop", this._desktopCount);
+  },
+};
+
 #ifdef CAN_DRAW_IN_TITLEBAR
 function updateTitlebarDisplay() {
 
 #ifdef XP_MACOSX
   // OS X and the other platforms differ enough to necessitate this kind of
   // special-casing. Like the other platforms where we CAN_DRAW_IN_TITLEBAR,
   // we draw in the OS X titlebar when putting the tabs up there. However, OS X
   // also draws in the titlebar when a lightweight theme is applied, regardless
--- a/browser/components/customizableui/CustomizeMode.jsm
+++ b/browser/components/customizableui/CustomizeMode.jsm
@@ -55,17 +55,16 @@ function CustomizeMode(aWindow) {
   this.areas = new Set();
 
   // There are two palettes - there's the palette that can be overlayed with
   // toolbar items in browser.xul. This is invisible, and never seen by the
   // user. Then there's the visible palette, which gets populated and displayed
   // to the user when in customizing mode.
   this.visiblePalette = this.document.getElementById(kPaletteId);
   this.paletteEmptyNotice = this.document.getElementById("customization-empty");
-  this.paletteSpacer = this.document.getElementById("customization-spacer");
   this.tipPanel = this.document.getElementById("customization-tipPanel");
   if (Services.prefs.getCharPref("general.skins.selectedSkin") != "classic/1.0") {
     let lwthemeButton = this.document.getElementById("customization-lwtheme-button");
     lwthemeButton.setAttribute("hidden", "true");
   }
 #ifdef CAN_DRAW_IN_TITLEBAR
   this._updateTitlebarButton();
   Services.prefs.addObserver(kDrawInTitlebarPref, this, false);
@@ -282,17 +281,16 @@ CustomizeMode.prototype = {
       // Show the palette now that the transition has finished.
       this.visiblePalette.hidden = false;
       window.setTimeout(() => {
         // Force layout reflow to ensure the animation runs,
         // and make it async so it doesn't affect the timing.
         this.visiblePalette.clientTop;
         this.visiblePalette.setAttribute("showing", "true");
       }, 0);
-      this.paletteSpacer.hidden = true;
       this._updateEmptyPaletteNotice();
 
       this.swatchForTheme(document);
       this.maybeShowTip(panelHolder);
 
       this._handler.isEnteringCustomizeMode = false;
       panelContents.removeAttribute("customize-transitioning");
 
@@ -361,17 +359,16 @@ CustomizeMode.prototype = {
 
     this._removePanelCustomizationPlaceholders();
 
     let window = this.window;
     let document = this.document;
     let documentElement = document.documentElement;
 
     // Hide the palette before starting the transition for increased perf.
-    this.paletteSpacer.hidden = false;
     this.visiblePalette.hidden = true;
     this.visiblePalette.removeAttribute("showing");
     this.paletteEmptyNotice.hidden = true;
 
     // Disable the button-text fade-out mask
     // during the transition for increased perf.
     let panelContents = window.PanelUI.contents;
     panelContents.setAttribute("customize-transitioning", "true");
--- a/browser/components/loop/content/css/panel.css
+++ b/browser/components/loop/content/css/panel.css
@@ -249,25 +249,23 @@ body {
 .room-list > .room-entry > h2 {
   display: inline-block;
   font-size: 1.3rem;
   line-height: 2.4rem;
   color: #000;
   /* See .room-entry-context-item for the margin/size reductions.
     * An extra 40px to make space for the call button and chevron. */
   width: calc(100% - 1rem - 56px);
-
 }
 
-.room-list > .room-entry.room-active > h2 {
+.room-list > .room-entry.room-active:not(.room-opened) > h2 {
   font-weight: bold;
-  color: #000;
 }
 
-.room-list > .room-entry:hover {
+.room-list > .room-entry:not(.room-opened):hover {
   background: #dbf7ff;
 }
 
 .room-list > .room-entry > p {
   margin: 0;
   padding: .2rem 0;
 }
 
@@ -412,17 +410,17 @@ html[dir="rtl"] .room-entry-context-acti
 /* Keep ".room-list > .room-entry > h2" in sync with these. */
 .room-entry-context-item {
   display: inline-block;
   vertical-align: middle;
   -moz-margin-start: 1rem;
   height: 16px;
 }
 
-.room-entry:hover .room-entry-context-item {
+.room-entry:not(.room-opened):hover .room-entry-context-item {
   display: none;
 }
 
 .room-entry-context-item > a > img {
   height: 16px;
   width: 16px;
 }
 
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -357,20 +357,24 @@ loop.panel = (function(_, mozL10n) {
           )
         )
       );
     }
   });
 
   /**
    * Room list entry.
+   *
+   * Active Room means there are participants in the room.
+   * Opened Room means the user is in the room.
    */
   var RoomEntry = React.createClass({displayName: "RoomEntry",
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+      isOpenedRoom: React.PropTypes.bool.isRequired,
       mozLoop: React.PropTypes.object.isRequired,
       room: React.PropTypes.instanceOf(loop.store.Room).isRequired
     },
 
     mixins: [
       loop.shared.mixins.WindowCloseMixin,
       sharedMixins.DropdownMenuMixin()
     ],
@@ -413,43 +417,46 @@ loop.panel = (function(_, mozL10n) {
       if (this.state.showMenu) {
         this.toggleDropdownMenu();
       }
     },
 
     render: function() {
       var roomClasses = React.addons.classSet({
         "room-entry": true,
-        "room-active": this._isActive()
+        "room-active": this._isActive(),
+        "room-opened": this.props.isOpenedRoom
       });
 
       var roomTitle = this.props.room.decryptedContext.roomName ||
         this.props.room.decryptedContext.urls[0].description ||
         this.props.room.decryptedContext.urls[0].location;
 
       return (
         React.createElement("div", {className: roomClasses, 
-          onClick: this.handleClickEntry, 
-          onMouseLeave: this._handleMouseOut, 
+          onClick: this.props.isOpenedRoom ? null : this.handleClickEntry, 
+          onMouseLeave: this.props.isOpenedRoom ? null : this._handleMouseOut, 
           ref: "roomEntry"}, 
           React.createElement("h2", null, 
             roomTitle
           ), 
           React.createElement(RoomEntryContextItem, {
             mozLoop: this.props.mozLoop, 
             roomUrls: this.props.room.decryptedContext.urls}), 
-          React.createElement(RoomEntryContextButtons, {
-            dispatcher: this.props.dispatcher, 
-            eventPosY: this.state.eventPosY, 
-            handleClickEntry: this.handleClickEntry, 
-            handleContextChevronClick: this.handleContextChevronClick, 
-            ref: "contextActions", 
-            room: this.props.room, 
-            showMenu: this.state.showMenu, 
-            toggleDropdownMenu: this.toggleDropdownMenu})
+          this.props.isOpenedRoom ? null :
+            React.createElement(RoomEntryContextButtons, {
+              dispatcher: this.props.dispatcher, 
+              eventPosY: this.state.eventPosY, 
+              handleClickEntry: this.handleClickEntry, 
+              handleContextChevronClick: this.handleContextChevronClick, 
+              ref: "contextActions", 
+              room: this.props.room, 
+              showMenu: this.state.showMenu, 
+              toggleDropdownMenu: this.toggleDropdownMenu})
+          
         )
       );
     }
   });
 
   /**
    * Buttons corresponding to each conversation entry.
    * This component renders the video icon call button and chevron button for
@@ -714,22 +721,30 @@ loop.panel = (function(_, mozL10n) {
 
       if (!this.state.rooms.length) {
         return this._renderNoRoomsView();
       }
 
       return (
         React.createElement("div", {className: "rooms"}, 
           this._renderNewRoomButton(), 
-          React.createElement("h1", null, mozL10n.get("rooms_list_recent_conversations")), 
+          React.createElement("h1", null, mozL10n.get(this.state.openedRoom === null ?
+                "rooms_list_recently_browsed" :
+                "rooms_list_currently_browsing")), 
           React.createElement("div", {className: "room-list"}, 
             this.state.rooms.map(function(room, i) {
+              if (this.state.openedRoom !== null &&
+                room.roomToken !== this.state.openedRoom) {
+                return null;
+              }
+
               return (
                 React.createElement(RoomEntry, {
                   dispatcher: this.props.dispatcher, 
+                  isOpenedRoom: room.roomToken === this.state.openedRoom, 
                   key: room.roomToken, 
                   mozLoop: this.props.mozLoop, 
                   room: room})
               );
             }, this)
           )
         )
       );
@@ -922,18 +937,18 @@ loop.panel = (function(_, mozL10n) {
       }
 
       return (
         React.createElement("div", {className: "panel-content"}, 
           React.createElement(NotificationListView, {
             clearOnDocumentHidden: true, 
             notifications: this.props.notifications}), 
             React.createElement(RoomList, {dispatcher: this.props.dispatcher, 
-                      mozLoop: this.props.mozLoop, 
-                      store: this.props.roomStore}), 
+              mozLoop: this.props.mozLoop, 
+              store: this.props.roomStore}), 
           React.createElement("div", {className: "footer"}, 
             React.createElement("div", {className: "user-details"}, 
               React.createElement(AccountLink, {fxAEnabled: this.props.mozLoop.fxAEnabled, 
                            userProfile: this.state.userProfile})
             ), 
             React.createElement("div", {className: "signin-details"}, 
               React.createElement(SettingsDropdown, {mozLoop: this.props.mozLoop})
             )
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -357,20 +357,24 @@ loop.panel = (function(_, mozL10n) {
           </a>
         </div>
       );
     }
   });
 
   /**
    * Room list entry.
+   *
+   * Active Room means there are participants in the room.
+   * Opened Room means the user is in the room.
    */
   var RoomEntry = React.createClass({
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+      isOpenedRoom: React.PropTypes.bool.isRequired,
       mozLoop: React.PropTypes.object.isRequired,
       room: React.PropTypes.instanceOf(loop.store.Room).isRequired
     },
 
     mixins: [
       loop.shared.mixins.WindowCloseMixin,
       sharedMixins.DropdownMenuMixin()
     ],
@@ -413,43 +417,46 @@ loop.panel = (function(_, mozL10n) {
       if (this.state.showMenu) {
         this.toggleDropdownMenu();
       }
     },
 
     render: function() {
       var roomClasses = React.addons.classSet({
         "room-entry": true,
-        "room-active": this._isActive()
+        "room-active": this._isActive(),
+        "room-opened": this.props.isOpenedRoom
       });
 
       var roomTitle = this.props.room.decryptedContext.roomName ||
         this.props.room.decryptedContext.urls[0].description ||
         this.props.room.decryptedContext.urls[0].location;
 
       return (
         <div className={roomClasses}
-          onClick={this.handleClickEntry}
-          onMouseLeave={this._handleMouseOut}
+          onClick={this.props.isOpenedRoom ? null : this.handleClickEntry}
+          onMouseLeave={this.props.isOpenedRoom ? null : this._handleMouseOut}
           ref="roomEntry">
           <h2>
             {roomTitle}
           </h2>
           <RoomEntryContextItem
             mozLoop={this.props.mozLoop}
             roomUrls={this.props.room.decryptedContext.urls} />
-          <RoomEntryContextButtons
-            dispatcher={this.props.dispatcher}
-            eventPosY={this.state.eventPosY}
-            handleClickEntry={this.handleClickEntry}
-            handleContextChevronClick={this.handleContextChevronClick}
-            ref="contextActions"
-            room={this.props.room}
-            showMenu={this.state.showMenu}
-            toggleDropdownMenu={this.toggleDropdownMenu} />
+          {this.props.isOpenedRoom ? null :
+            <RoomEntryContextButtons
+              dispatcher={this.props.dispatcher}
+              eventPosY={this.state.eventPosY}
+              handleClickEntry={this.handleClickEntry}
+              handleContextChevronClick={this.handleContextChevronClick}
+              ref="contextActions"
+              room={this.props.room}
+              showMenu={this.state.showMenu}
+              toggleDropdownMenu={this.toggleDropdownMenu} />
+          }
         </div>
       );
     }
   });
 
   /**
    * Buttons corresponding to each conversation entry.
    * This component renders the video icon call button and chevron button for
@@ -714,22 +721,30 @@ loop.panel = (function(_, mozL10n) {
 
       if (!this.state.rooms.length) {
         return this._renderNoRoomsView();
       }
 
       return (
         <div className="rooms">
           {this._renderNewRoomButton()}
-          <h1>{mozL10n.get("rooms_list_recent_conversations")}</h1>
+          <h1>{mozL10n.get(this.state.openedRoom === null ?
+                "rooms_list_recently_browsed" :
+                "rooms_list_currently_browsing")}</h1>
           <div className="room-list">{
             this.state.rooms.map(function(room, i) {
+              if (this.state.openedRoom !== null &&
+                room.roomToken !== this.state.openedRoom) {
+                return null;
+              }
+
               return (
                 <RoomEntry
                   dispatcher={this.props.dispatcher}
+                  isOpenedRoom={room.roomToken === this.state.openedRoom}
                   key={room.roomToken}
                   mozLoop={this.props.mozLoop}
                   room={room} />
               );
             }, this)
           }</div>
         </div>
       );
@@ -922,18 +937,18 @@ loop.panel = (function(_, mozL10n) {
       }
 
       return (
         <div className="panel-content">
           <NotificationListView
             clearOnDocumentHidden={true}
             notifications={this.props.notifications} />
             <RoomList dispatcher={this.props.dispatcher}
-                      mozLoop={this.props.mozLoop}
-                      store={this.props.roomStore} />
+              mozLoop={this.props.mozLoop}
+              store={this.props.roomStore} />
           <div className="footer">
             <div className="user-details">
               <AccountLink fxAEnabled={this.props.mozLoop.fxAEnabled}
                            userProfile={this.state.userProfile}/>
             </div>
             <div className="signin-details">
               <SettingsDropdown mozLoop={this.props.mozLoop}/>
             </div>
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -8,16 +8,17 @@ describe("loop.panel", function() {
   var expect = chai.expect;
   var TestUtils = React.addons.TestUtils;
   var sharedActions = loop.shared.actions;
   var sharedUtils = loop.shared.utils;
 
   var sandbox, notifications;
   var fakeXHR, fakeWindow, fakeMozLoop, fakeEvent;
   var requests = [];
+  var roomData, roomData2, roomList, roomName;
   var mozL10nGetSpy;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
     fakeXHR = sandbox.useFakeXMLHttpRequest();
     requests = [];
     // https://github.com/cjohansen/Sinon.JS/issues/393
     fakeXHR.xhr.onCreate = function(xhr) {
@@ -68,16 +69,55 @@ describe("loop.panel", function() {
       logInToFxA: sandbox.stub(),
       logOutFromFxA: sandbox.stub(),
       notifyUITour: sandbox.stub(),
       openURL: sandbox.stub(),
       getSelectedTabMetadata: sandbox.stub(),
       userProfile: null
     };
 
+    roomName = "First Room Name";
+    roomData = {
+      roomToken: "QzBbvGmIZWU",
+      roomUrl: "http://sample/QzBbvGmIZWU",
+      decryptedContext: {
+        roomName: roomName
+      },
+      maxSize: 2,
+        participants: [{
+        displayName: "Alexis",
+          account: "alexis@example.com",
+          roomConnectionId: "2a1787a6-4a73-43b5-ae3e-906ec1e763cb"
+      }, {
+        displayName: "Adam",
+        roomConnectionId: "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7"
+    }],
+      ctime: 1405517418
+    };
+
+    roomData2 = {
+      roomToken: "QzBbvlmIZWU",
+      roomUrl: "http://sample/QzBbvlmIZWU",
+      decryptedContext: {
+        roomName: "Second Room Name"
+      },
+      maxSize: 2,
+        participants: [{
+        displayName: "Bill",
+          account: "bill@example.com",
+          roomConnectionId: "2a1737a6-4a73-43b5-ae3e-906ec1e763cb"
+      }, {
+          displayName: "Bob",
+            roomConnectionId: "781f212b-f1ea-4ce1-9105-7cfc36fb4ec7"
+        }],
+      ctime: 1405517417
+    };
+
+    roomList = [new loop.store.Room(roomData), new loop.store.Room(roomData2)];
+
     document.mozL10n.initialize(navigator.mozLoop);
     sandbox.stub(document.mozL10n, "get").returns("Fake title");
   });
 
   afterEach(function() {
     delete navigator.mozLoop;
     loop.shared.mixins.setRootObject(window);
     sandbox.restore();
@@ -533,35 +573,20 @@ describe("loop.panel", function() {
         } catch (ex) {
           // Do nothing
         }
       });
     });
   });
 
   describe("loop.panel.RoomEntry", function() {
-    var dispatcher, roomData;
+    var dispatcher;
 
     beforeEach(function() {
       dispatcher = new loop.Dispatcher();
-      roomData = {
-        roomToken: "QzBbvGmIZWU",
-        roomUrl: "http://sample/QzBbvGmIZWU",
-        decryptedContext: {
-          roomName: "Second Room Name"
-        },
-        maxSize: 2,
-        participants: [
-          { displayName: "Alexis", account: "alexis@example.com",
-            roomConnectionId: "2a1787a6-4a73-43b5-ae3e-906ec1e763cb" },
-          { displayName: "Adam",
-            roomConnectionId: "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7" }
-        ],
-        ctime: 1405517418
-      };
     });
 
     function mountRoomEntry(props) {
       props = _.extend({
         dispatcher: dispatcher,
         mozLoop: fakeMozLoop
       }, props);
       return TestUtils.renderIntoDocument(
@@ -571,17 +596,20 @@ describe("loop.panel", function() {
     describe("handleContextChevronClick", function() {
       var view;
 
       beforeEach(function() {
         // Stub to prevent warnings due to stores not being set up to handle
         // the actions we are triggering.
         sandbox.stub(dispatcher, "dispatch");
 
-        view = mountRoomEntry({ room: new loop.store.Room(roomData) });
+        view = mountRoomEntry({
+          isOpenedRoom: false,
+          room: new loop.store.Room(roomData)
+        });
       });
 
       // XXX Current version of React cannot use TestUtils.Simulate, please
       // enable when we upgrade.
       it.skip("should close the menu when you move out the cursor", function() {
         expect(view.refs.contextActions.state.showMenu).to.eql(false);
       });
 
@@ -613,16 +641,17 @@ describe("loop.panel", function() {
 
       beforeEach(function() {
         // Stub to prevent warnings where no stores are set up to handle the
         // actions we are testing.
         sandbox.stub(dispatcher, "dispatch");
 
         roomEntry = mountRoomEntry({
           deleteRoom: sandbox.stub(),
+          isOpenedRoom: false,
           room: new loop.store.Room(roomData)
         });
       });
 
       it("should render context actions button", function() {
         expect(roomEntry.refs.contextActions).to.not.eql(null);
       });
 
@@ -643,24 +672,37 @@ describe("loop.panel", function() {
             new sharedActions.OpenRoom({ roomToken: roomData.roomToken }));
         });
 
         it("should call window.close", function() {
           roomEntry.handleClickEntry(fakeEvent);
 
           sinon.assert.calledOnce(fakeWindow.close);
         });
+
+        it("should not dispatch an OpenRoom action when button is clicked if room is already opened", function() {
+          roomEntry = mountRoomEntry({
+            deleteRoom: sandbox.stub(),
+            isOpenedRoom: true,
+            room: new loop.store.Room(roomData)
+          });
+
+          TestUtils.Simulate.click(roomEntry.refs.roomEntry.getDOMNode());
+
+          sinon.assert.notCalled(dispatcher.dispatch);
+        });
       });
     });
 
     describe("Context Indicator", function() {
       var roomEntry;
 
       function mountEntryForContext() {
         return mountRoomEntry({
+          isOpenedRoom: false,
           room: new loop.store.Room(roomData)
         });
       }
 
       it("should not display a context indicator if the room doesn't have any", function() {
         roomEntry = mountEntryForContext();
 
         expect(roomEntry.getDOMNode().querySelector(".room-entry-context-item")).eql(null);
@@ -711,27 +753,29 @@ describe("loop.panel", function() {
     describe("Room Entry click", function() {
       var roomEntry, roomEntryNode;
 
       beforeEach(function() {
         sandbox.stub(dispatcher, "dispatch");
 
         roomEntry = mountRoomEntry({
           dispatcher: dispatcher,
+          isOpenedRoom: false,
           room: new loop.store.Room(roomData)
         });
         roomEntryNode = roomEntry.getDOMNode();
       });
 
     });
 
     describe("Room name updated", function() {
       it("should update room name", function() {
         var roomEntry = mountRoomEntry({
           dispatcher: dispatcher,
+          isOpenedRoom: false,
           room: new loop.store.Room(roomData)
         });
         var updatedRoom = new loop.store.Room(_.extend({}, roomData, {
           decryptedContext: {
             roomName: "New room name"
           },
           ctime: new Date().getTime()
         }));
@@ -744,16 +788,17 @@ describe("loop.panel", function() {
       });
     });
 
     describe("Room name priority", function() {
       var roomEntry;
       beforeEach(function() {
         roomEntry = mountRoomEntry({
           dispatcher: dispatcher,
+          isOpenedRoom: false,
           room: new loop.store.Room(roomData)
         });
       });
 
       function setDecryptedContext(newDecryptedContext) {
         return new loop.store.Room(_.extend({}, roomData, {
           decryptedContext: newDecryptedContext,
           ctime: new Date().getTime()
@@ -803,50 +848,33 @@ describe("loop.panel", function() {
         roomEntry.setProps({ room: updatedRoom });
 
         expect(roomEntry.getDOMNode().textContent).eql("https://fakeurl.com");
       });
     });
   });
 
   describe("loop.panel.RoomList", function() {
-    var roomStore, dispatcher, fakeEmail, dispatch, roomData;
+    var roomStore, dispatcher, fakeEmail, dispatch;
 
     beforeEach(function() {
       fakeEmail = "fakeEmail@example.com";
       dispatcher = new loop.Dispatcher();
       roomStore = new loop.store.RoomStore(dispatcher, {
         mozLoop: navigator.mozLoop
       });
       roomStore.setStoreState({
+        openedRoom: null,
         pendingCreation: false,
         pendingInitialRetrieval: false,
         rooms: [],
         error: undefined
       });
 
       dispatch = sandbox.stub(dispatcher, "dispatch");
-
-      roomData = {
-        roomToken: "QzBbvGmIZWU",
-        roomUrl: "http://sample/QzBbvGmIZWU",
-        decryptedContext: {
-          roomName: "Second Room Name"
-        },
-        maxSize: 2,
-        participants: [{
-          displayName: "Alexis",
-          account: "alexis@example.com",
-          roomConnectionId: "2a1787a6-4a73-43b5-ae3e-906ec1e763cb"
-        }, {
-          displayName: "Adam",
-          roomConnectionId: "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7"
-        }],
-        ctime: 1405517418
-      };
     });
 
     function createTestComponent() {
       return TestUtils.renderIntoDocument(
         React.createElement(loop.panel.RoomList, {
           store: roomStore,
           dispatcher: dispatcher,
           userDisplayName: fakeEmail,
@@ -891,16 +919,37 @@ describe("loop.panel", function() {
     });
 
     it("should display a loading animation when rooms are pending", function() {
       var view = createTestComponent();
       roomStore.setStoreState({ pendingInitialRetrieval: true });
 
       expect(view.getDOMNode().querySelectorAll(".room-list-loading").length).to.eql(1);
     });
+
+    it("should show multiple rooms in list with no opened room", function() {
+      roomStore.setStoreState({ rooms: roomList });
+
+      var view = createTestComponent();
+
+      var node = view.getDOMNode();
+      expect(node.querySelectorAll(".room-opened").length).to.eql(0);
+      expect(node.querySelectorAll(".room-entry").length).to.eql(2);
+    });
+
+    it("should only show the opened room you're in when you're in a room", function() {
+      roomStore.setStoreState({ rooms: roomList, openedRoom: roomList[0].roomToken });
+
+      var view = createTestComponent();
+
+      var node = view.getDOMNode();
+      expect(node.querySelectorAll(".room-opened").length).to.eql(1);
+      expect(node.querySelectorAll(".room-entry").length).to.eql(1);
+      expect(node.querySelectorAll(".room-opened h2")[0].textContent).to.equal(roomName);
+    });
   });
 
   describe("loop.panel.NewRoomView", function() {
     var roomStore, dispatcher, fakeEmail, dispatch;
 
     beforeEach(function() {
       fakeEmail = "fakeEmail@example.com";
       dispatcher = new loop.Dispatcher();
@@ -1069,51 +1118,33 @@ describe("loop.panel", function() {
        function() {
          TestUtils.Simulate.click(view.refs.deleteButton.getDOMNode());
 
          sinon.assert.calledOnce(view.props.handleDeleteButtonClick);
        });
   });
 
   describe("RoomEntryContextButtons", function() {
-    var view, dispatcher, roomData;
+    var view, dispatcher;
 
     function createTestComponent(extraProps) {
       var props = _.extend({
         dispatcher: dispatcher,
         eventPosY: 0,
         handleClickEntry: sandbox.stub(),
         showMenu: false,
         room: roomData,
         toggleDropdownMenu: sandbox.stub(),
         handleContextChevronClick: sandbox.stub()
       }, extraProps);
       return TestUtils.renderIntoDocument(
         React.createElement(loop.panel.RoomEntryContextButtons, props));
     }
 
     beforeEach(function() {
-      roomData = {
-        roomToken: "QzBbvGmIZWU",
-        roomUrl: "http://sample/QzBbvGmIZWU",
-        decryptedContext: {
-          roomName: "Second Room Name"
-        },
-        maxSize: 2,
-        participants: [{
-          displayName: "Alexis",
-          account: "alexis@example.com",
-          roomConnectionId: "2a1787a6-4a73-43b5-ae3e-906ec1e763cb"
-        }, {
-          displayName: "Adam",
-          roomConnectionId: "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7"
-        }],
-        ctime: 1405517418
-      };
-
       dispatcher = new loop.Dispatcher();
       sandbox.stub(dispatcher, "dispatch");
 
       view = createTestComponent();
     });
 
     it("should render ConversationDropdown if state.showMenu=true", function() {
       view = createTestComponent({ showMenu: true });
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -519,22 +519,16 @@ BrowserGlue.prototype = {
               break;
             }
           }
         });
         break;
       case "autocomplete-did-enter-text":
         this._handleURLBarTelemetry(subject.QueryInterface(Ci.nsIAutoCompleteInput));
         break;
-      case "tablet-mode-change":
-        if (data == "tablet-mode") {
-          Services.telemetry.getHistogramById("FX_TABLET_MODE_USED_DURING_SESSION")
-                            .add(1);
-        }
-        break;
       case "test-initialize-sanitizer":
         this._sanitizer.onStartup();
         break;
     }
   },
 
   _handleURLBarTelemetry(input) {
     if (!input ||
@@ -635,17 +629,16 @@ BrowserGlue.prototype = {
     os.addObserver(this, "keyword-search", false);
 #endif
     os.addObserver(this, "browser-search-engine-modified", false);
     os.addObserver(this, "browser-search-service", false);
     os.addObserver(this, "restart-in-safe-mode", false);
     os.addObserver(this, "flash-plugin-hang", false);
     os.addObserver(this, "xpi-signature-changed", false);
     os.addObserver(this, "autocomplete-did-enter-text", false);
-    os.addObserver(this, "tablet-mode-change", false);
 
     ExtensionManagement.registerScript("chrome://browser/content/ext-utils.js");
     ExtensionManagement.registerScript("chrome://browser/content/ext-browserAction.js");
     ExtensionManagement.registerScript("chrome://browser/content/ext-pageAction.js");
     ExtensionManagement.registerScript("chrome://browser/content/ext-contextMenus.js");
     ExtensionManagement.registerScript("chrome://browser/content/ext-tabs.js");
     ExtensionManagement.registerScript("chrome://browser/content/ext-windows.js");
     ExtensionManagement.registerScript("chrome://browser/content/ext-bookmarks.js");
@@ -694,17 +687,16 @@ BrowserGlue.prototype = {
       // may have already been removed by the observer
     } catch (ex) {}
 #ifdef NIGHTLY_BUILD
     Services.prefs.removeObserver(POLARIS_ENABLED, this);
 #endif
     os.removeObserver(this, "flash-plugin-hang");
     os.removeObserver(this, "xpi-signature-changed");
     os.removeObserver(this, "autocomplete-did-enter-text");
-    os.removeObserver(this, "tablet-mode-change");
   },
 
   _onAppDefaults: function BG__onAppDefaults() {
     // apply distribution customizations (prefs)
     // other customizations are applied in _finalUIStartup()
     this._distributionCustomizer.applyPrefDefaults();
   },
 
@@ -1075,23 +1067,16 @@ BrowserGlue.prototype = {
     let SCALING_PROBE_NAME = "DISPLAY_SCALING_LINUX";
 #else
     let SCALING_PROBE_NAME = "";
 #endif
     if (SCALING_PROBE_NAME) {
       let scaling = aWindow.devicePixelRatio * 100;
       Services.telemetry.getHistogramById(SCALING_PROBE_NAME).add(scaling);
     }
-
-#ifdef XP_WIN
-    if (WindowsUIUtils.inTabletMode) {
-      Services.telemetry.getHistogramById("FX_TABLET_MODE_USED_DURING_SESSION")
-                        .add(1);
-    }
-#endif
   },
 
   // the first browser window has finished initializing
   _onFirstWindowLoaded: function BG__onFirstWindowLoaded(aWindow) {
     // Initialize PdfJs when running in-process and remote. This only
     // happens once since PdfJs registers global hooks. If the PdfJs
     // extension is installed the init method below will be overridden
     // leaving initialization to the extension.
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -178,19 +178,22 @@ feedback_request_button=Leave Feedback
 
 help_label=Help
 tour_label=Tour
 
 ## LOCALIZATION NOTE(rooms_default_room_name_template): {{conversationLabel}}
 ## will be replaced by a number. For example "Conversation 1" or "Conversation 12".
 rooms_default_room_name_template=Conversation {{conversationLabel}}
 rooms_leave_button_label=Leave
-## LOCALIZATION NOTE (rooms_list_recent_conversations): String is in all caps
+## LOCALIZATION NOTE (rooms_list_recently_browsed): String is in all caps
 ## for emphasis reasons, it is a heading. Proceed as appropriate for locale.
-rooms_list_recent_conversations=RECENT CONVERSATIONS
+rooms_list_recently_browsed=RECENTLY BROWSED
+## LOCALIZATION NOTE (rooms_list_currently_browsing): String is in all caps
+## for emphasis reasons, it is a heading. Proceed as appropriate for locale.
+rooms_list_currently_browsing=CURRENTLY BROWSING
 rooms_change_failed_label=Conversation cannot be updated
 rooms_panel_title=Choose a conversation or start a new one
 rooms_room_full_label=There are already two people in this conversation.
 rooms_room_full_call_to_action_nonFx_label=Download {{brandShortname}} to start your own
 rooms_room_full_call_to_action_label=Learn more about {{clientShortname}} ยป
 rooms_room_joined_label=Someone has joined the conversation!
 rooms_room_join_label=Join the conversation
 rooms_signout_alert=Open conversations will be closed
--- a/browser/themes/linux/searchbar.css
+++ b/browser/themes/linux/searchbar.css
@@ -80,30 +80,30 @@ menuitem[cmd="cmd_clearhistory"][disable
 
 
 .search-panel-current-engine {
   border-top: none !important;
   -moz-box-align: center;
 }
 
 .search-panel-current-engine {
-  border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+  border-bottom: none;
+}
+
+.search-panel-tree {
+  border-top: 1px solid #ccc !important;
 }
 
 .search-panel-header {
   font-weight: normal;
   border-top: 1px solid rgba(0, 0, 0, 0.2);
   padding: 3px 5px;
   color: MenuText;
 }
 
-.search-panel-tree[collapsed=true] + .search-panel-header {
-  border-top: none;
-}
-
 .search-panel-header > label {
   margin-top: 2px !important;
   margin-bottom: 1px !important;
 }
 
 .search-panel-current-input > label {
   margin: 2px 0 1px !important;
 }
--- a/browser/themes/osx/searchbar.css
+++ b/browser/themes/osx/searchbar.css
@@ -105,33 +105,33 @@
 }
 
 .search-panel-current-engine {
   border-top: none !important;
   border-radius: 4px 4px 0 0;
 }
 
 .search-panel-current-engine {
-  border-bottom: 1px solid #ccc;
+  border-bottom: none;
+}
+
+.search-panel-tree {
+  border-top: 1px solid #ccc !important;
 }
 
 .search-panel-header {
   font-size: 10px;
   font-weight: normal;
   background-color: rgb(245, 245, 245);
   border-top: 1px solid #ccc;
   margin: 0;
   padding: 3px 6px;
   color: #666;
 }
 
-.search-panel-tree[collapsed=true] + .search-panel-header {
-  border-top: none;
-}
-
 .search-panel-header > label {
   margin-top: 2px !important;
   margin-bottom: 2px !important;
 }
 
 .search-panel-current-input > label {
   margin: 2px 0 !important;
 }
--- a/browser/themes/shared/social/chat-icons.svg
+++ b/browser/themes/shared/social/chat-icons.svg
@@ -37,13 +37,15 @@
   <use id="close-active" xlink:href="#close-shape"/>
   <use id="close-disabled" xlink:href="#close-shape"/>
   <use id="close-hover" xlink:href="#close-shape"/>
   <use id="exit-white" xlink:href="#exit-shape"/>
   <use id="expand" xlink:href="#expand-shape"/>
   <use id="expand-active" xlink:href="#expand-shape"/>
   <use id="expand-disabled" xlink:href="#expand-shape"/>
   <use id="expand-hover" xlink:href="#expand-shape"/>
+  <use id="expand-white" xlink:href="#expand-shape"/>
   <use id="minimize" xlink:href="#minimize-shape"/>
   <use id="minimize-active" xlink:href="#minimize-shape"/>
   <use id="minimize-disabled" xlink:href="#minimize-shape"/>
   <use id="minimize-hover" xlink:href="#minimize-shape"/>
+  <use id="minimize-white" xlink:href="#minimize-shape"/>
 </svg>
--- a/browser/themes/shared/social/chat.inc.css
+++ b/browser/themes/shared/social/chat.inc.css
@@ -100,16 +100,24 @@
 .chat-swap-button:hover:active {
   list-style-image: url("chrome://browser/skin/social/chat-icons.svg#expand-active");
 }
 
 chatbar > chatbox > .chat-titlebar > .chat-swap-button {
   transform: none;
 }
 
+chatbox[src^="about:loopconversation#"] .chat-minimize-button {
+  list-style-image: url("chrome://browser/skin/social/chat-icons.svg#minimize-white");
+}
+
+chatbox[src^="about:loopconversation#"] .chat-swap-button {
+  list-style-image: url("chrome://browser/skin/social/chat-icons.svg#expand-white");
+}
+
 .chat-loop-hangup {
   list-style-image: url("chrome://browser/skin/social/chat-icons.svg#exit-white");
   background-color: #d13f1a;
   border: 1px solid #d13f1a;
   border-top-right-radius: 4px;
   width: 32px;
   height: 26px;
   margin-top: -6px;
@@ -124,16 +132,20 @@ chatbar > chatbox > .chat-titlebar > .ch
 }
 
 .chat-title {
   color: #666;
   text-shadow: none;
   cursor: inherit;
 }
 
+chatbox[src^="about:loopconversation#"] .chat-title {
+  color: white;
+}
+
 .chat-titlebar {
   height: 26px;
   min-height: 26px;
   width: 100%;
   margin: 0;
   padding: 5px 4px;
   border: 1px solid #ebebeb;
   border-bottom: 0;
@@ -142,16 +154,21 @@ chatbar > chatbox > .chat-titlebar > .ch
   cursor: pointer;
   background-color: #ebebeb;
 }
 
 .chat-titlebar[selected] {
   background-color: #f0f0f0;
 }
 
+chatbox[src^="about:loopconversation#"] > .chat-titlebar {
+  background-color: #00a9dc;
+  border-color: #00a9dc;
+}
+
 .chat-titlebar > .notification-anchor-icon {
   margin-left: 2px;
   margin-right: 2px;
 }
 
 .chat-titlebar[minimized="true"] {
   border-bottom: none;
 }
--- a/browser/themes/windows/searchbar.css
+++ b/browser/themes/windows/searchbar.css
@@ -113,32 +113,32 @@
 }
 
 .search-panel-current-engine {
   border-top: none !important;
   -moz-box-align: center;
 }
 
 .search-panel-current-engine {
-  border-bottom: 1px solid #ccc;
+  border-bottom: none;
+}
+
+.search-panel-tree {
+  border-top: 1px solid #ccc !important;
 }
 
 .search-panel-header {
   font-weight: normal;
   background-color: rgb(245, 245, 245);
   border-top: 1px solid #ccc;
   margin: 0;
   padding: 3px 6px;
   color: #666;
 }
 
-.search-panel-tree[collapsed=true] + .search-panel-header {
-  border-top: none;
-}
-
 .search-panel-header > label {
   margin-top: 2px !important;
   margin-bottom: 1px !important;
 }
 
 .search-panel-current-input > label {
   margin: 2px 0 1px !important;
 }
--- a/devtools/.eslintrc
+++ b/devtools/.eslintrc
@@ -21,19 +21,21 @@
     "XPCNativeWrapper": true,
     "XPCOMUtils": true,
   },
   "rules": {
     // These are the rules that have been configured so far to match the
     // devtools coding style.
 
     // Rules from the mozilla plugin
+    "mozilla/balanced-listeners": 2,
     "mozilla/components-imports": 1,
     "mozilla/import-headjs-globals": 1,
     "mozilla/mark-test-function-used": 1,
+    "mozilla/no-aArgs": 1,
     "mozilla/var-only-at-top-level": 1,
 
     // Disallow using variables outside the blocks they are defined (especially
     // since only let and const are used, see "no-var").
     "block-scoped-var": 2,
     // Enforce one true brace style (opening brace on the same line) and avoid
     // start and end braces on the same line.
     "brace-style": [2, "1tbs", {"allowSingleLine": false}],
--- a/devtools/client/markupview/markup-view.css
+++ b/devtools/client/markupview/markup-view.css
@@ -72,36 +72,40 @@ body.dragging .tag-line {
 }
 
 /* Normally this element takes space in the layout even if it's position: relative
  * by adding height: 0 we let surrounding elements to fill the blank space */
 .child.dragging {
   position: relative;
   pointer-events: none;
   opacity: 0.7;
+  z-index: 1;
   height: 0;
 }
 
 /* Indicates a tag-line in the markup-view as being an active drop target by
  * drawing a horizontal line where the dragged element would be inserted if
  * dropped here */
-.tag-line.drop-target::before, .tag-line.drag-target::before {
+.tag-line.drop-target::before,
+.tag-line.drag-target::before {
   content: '';
   position: absolute;
-  left: 0;
   top: 0;
   width: 100%;
+  /* Offset these by 1000px to make sure they cover the full width of the view */
+  padding-left: 1000px;
+  left: -1000px;
 }
 
 .tag-line.drag-target::before {
-  border-top: 2px dashed var(--theme-contrast-background);
+  border-top: 2px solid var(--theme-content-color2);
 }
 
 .tag-line.drop-target::before {
-  border-top: 2px dashed var(--theme-content-color1);
+  border-top: 2px solid var(--theme-contrast-background);
 }
 
 /* In case the indicator is put on the closing .tag-line, the indentation level
  * will become misleading, so we push it forward to match the indentation level */
 ul.children + .tag-line::before {
   margin-left: 14px;
 }
 
--- a/devtools/client/markupview/markup-view.js
+++ b/devtools/client/markupview/markup-view.js
@@ -1,48 +1,49 @@
 /* -*- 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";
 
 const {Cc, Cu, Ci} = require("chrome");
 
 // Page size for pageup/pagedown
 const PAGE_SIZE = 10;
 const DEFAULT_MAX_CHILDREN = 100;
 const COLLAPSE_ATTRIBUTE_LENGTH = 120;
 const COLLAPSE_DATA_URL_REGEX = /^data.+base64/;
 const COLLAPSE_DATA_URL_LENGTH = 60;
 const NEW_SELECTION_HIGHLIGHTER_TIMER = 1000;
 const DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE = 50;
 const DRAG_DROP_MIN_AUTOSCROLL_SPEED = 5;
 const DRAG_DROP_MAX_AUTOSCROLL_SPEED = 15;
+const DRAG_DROP_MIN_INITIAL_DISTANCE = 10;
 const AUTOCOMPLETE_POPUP_PANEL_ID = "markupview_autoCompletePopup";
 
 const {UndoStack} = require("devtools/client/shared/undo");
 const {editableField, InplaceEditor} = require("devtools/client/shared/inplace-editor");
-const {gDevTools} = Cu.import("resource://devtools/client/framework/gDevTools.jsm", {});
 const {HTMLEditor} = require("devtools/client/markupview/html-editor");
 const promise = require("promise");
 const {Tooltip} = require("devtools/client/shared/widgets/Tooltip");
 const EventEmitter = require("devtools/shared/event-emitter");
 const Heritage = require("sdk/core/heritage");
 const {setTimeout, clearTimeout, setInterval, clearInterval} = require("sdk/timers");
 const {parseAttribute} = require("devtools/client/shared/node-attribute-parser");
 const ELLIPSIS = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data;
 const {Task} = require("resource://gre/modules/Task.jsm");
 const {scrollIntoViewIfNeeded} = require("devtools/shared/layout/utils");
 
 Cu.import("resource://devtools/shared/gcli/Templater.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 loader.lazyGetter(this, "DOMParser", function() {
- return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
+  return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
 });
 loader.lazyGetter(this, "AutocompletePopup", () => {
   return require("devtools/client/shared/autocomplete-popup").AutocompletePopup;
 });
 
 /**
  * Vocabulary for the purposes of this file:
  *
@@ -70,17 +71,17 @@ function MarkupView(aInspector, aFrame, 
   this._frame = aFrame;
   this.win = this._frame.contentWindow;
   this.doc = this._frame.contentDocument;
   this._elt = this.doc.querySelector("#root");
   this.htmlEditor = new HTMLEditor(this.doc);
 
   try {
     this.maxChildren = Services.prefs.getIntPref("devtools.markup.pagesize");
-  } catch(ex) {
+  } catch (ex) {
     this.maxChildren = DEFAULT_MAX_CHILDREN;
   }
 
   // Creating the popup to be used to show CSS suggestions.
   let options = {
     autoSelect: true,
     theme: "auto",
     // panelId option prevents the markupView autocomplete popup from
@@ -89,142 +90,92 @@ function MarkupView(aInspector, aFrame, 
   };
   this.popup = new AutocompletePopup(this.doc.defaultView.parent.document, options);
 
   this.undo = new UndoStack();
   this.undo.installController(aControllerWindow);
 
   this._containers = new Map();
 
-  this._boundMutationObserver = this._mutationObserver.bind(this);
-  this.walker.on("mutations", this._boundMutationObserver);
-
-  this._boundOnDisplayChange = this._onDisplayChange.bind(this);
-  this.walker.on("display-change", this._boundOnDisplayChange);
-
+  // Binding functions that need to be called in scope.
+  this._mutationObserver = this._mutationObserver.bind(this);
+  this._onDisplayChange = this._onDisplayChange.bind(this);
   this._onMouseClick = this._onMouseClick.bind(this);
-
   this._onMouseUp = this._onMouseUp.bind(this);
+  this._onNewSelection = this._onNewSelection.bind(this);
+  this._onKeyDown = this._onKeyDown.bind(this);
+  this._onCopy = this._onCopy.bind(this);
+  this._onFocus = this._onFocus.bind(this);
+  this._onMouseMove = this._onMouseMove.bind(this);
+  this._onMouseLeave = this._onMouseLeave.bind(this);
+  this._onToolboxPickerHover = this._onToolboxPickerHover.bind(this);
+
+  // Listening to various events.
+  this._elt.addEventListener("click", this._onMouseClick, false);
+  this._elt.addEventListener("mousemove", this._onMouseMove, false);
+  this._elt.addEventListener("mouseleave", this._onMouseLeave, false);
   this.doc.body.addEventListener("mouseup", this._onMouseUp);
-
-  this._boundOnNewSelection = this._onNewSelection.bind(this);
-  this._inspector.selection.on("new-node-front", this._boundOnNewSelection);
+  this.win.addEventListener("keydown", this._onKeyDown, false);
+  this.win.addEventListener("copy", this._onCopy);
+  this._frame.addEventListener("focus", this._onFocus, false);
+  this.walker.on("mutations", this._mutationObserver);
+  this.walker.on("display-change", this._onDisplayChange);
+  this._inspector.selection.on("new-node-front", this._onNewSelection);
+  this._inspector.toolbox.on("picker-node-hovered", this._onToolboxPickerHover);
+
   this._onNewSelection();
-
-  this._boundKeyDown = this._onKeyDown.bind(this);
-  this._frame.contentWindow.addEventListener("keydown", this._boundKeyDown, false);
-
-  this._onCopy = this._onCopy.bind(this);
-  this._frame.contentWindow.addEventListener("copy", this._onCopy);
-
-  this._boundFocus = this._onFocus.bind(this);
-  this._frame.addEventListener("focus", this._boundFocus, false);
-
-  this._makeTooltipPersistent = this._makeTooltipPersistent.bind(this);
-
   this._initTooltips();
-  this._initHighlighter();
 
   EventEmitter.decorate(this);
 }
 
 exports.MarkupView = MarkupView;
 
 MarkupView.prototype = {
   /**
    * How long does a node flash when it mutates (in ms).
    */
   CONTAINER_FLASHING_DURATION: 500,
-  /**
-   * How long do you have to hold the mouse down before a drag
-   * starts (in ms).
-   */
-  GRAB_DELAY: 400,
 
   _selectedContainer: null,
 
   _initTooltips: function() {
     this.tooltip = new Tooltip(this._inspector.panelDoc);
     this._makeTooltipPersistent(false);
-
-    this._elt.addEventListener("click", this._onMouseClick, false);
-  },
-
-  _initHighlighter: function() {
-    // Show the box model on markup-view mousemove
-    this._onMouseMove = this._onMouseMove.bind(this);
-    this._elt.addEventListener("mousemove", this._onMouseMove, false);
-    this._onMouseLeave = this._onMouseLeave.bind(this);
-    this._elt.addEventListener("mouseleave", this._onMouseLeave, false);
-
-    // Show markup-containers as hovered on toolbox "picker-node-hovered" event
-    // which happens when the "pick" button is pressed
-    this._onToolboxPickerHover = (event, nodeFront) => {
-      this.showNode(nodeFront).then(() => {
-        this._showContainerAsHovered(nodeFront);
-      });
-    };
-    this._inspector.toolbox.on("picker-node-hovered", this._onToolboxPickerHover);
   },
 
   _makeTooltipPersistent: function(state) {
     if (state) {
       this.tooltip.stopTogglingOnHover();
     } else {
       this.tooltip.startTogglingOnHover(this._elt,
         this._isImagePreviewTarget.bind(this));
     }
   },
 
+  _onToolboxPickerHover: function(event, nodeFront) {
+    this.showNode(nodeFront).then(() => {
+      this._showContainerAsHovered(nodeFront);
+    }, e => console.error(e));
+  },
+
   isDragging: false,
 
   _onMouseMove: function(event) {
+    let target = event.target;
+
+    // Auto-scroll if we're dragging.
     if (this.isDragging) {
       event.preventDefault();
-      this._dragStartEl = event.target;
-
-      let docEl = this.doc.documentElement;
-
-      if (this._scrollInterval) {
-        clearInterval(this._scrollInterval);
-      }
-
-      // Auto-scroll when the mouse approaches top/bottom edge
-      let distanceFromBottom = docEl.clientHeight - event.pageY + this.win.scrollY,
-          distanceFromTop = event.pageY - this.win.scrollY;
-
-      if (distanceFromBottom <= DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE) {
-        // Map our distance from 0-50 to 5-15 range so the speed is kept
-        // in a range not too fast, not too slow
-        let speed = map(distanceFromBottom, 0, DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE,
-                        DRAG_DROP_MIN_AUTOSCROLL_SPEED, DRAG_DROP_MAX_AUTOSCROLL_SPEED);
-        // Here, we use minus because the value of speed - 15 is always negative
-        // and it makes the speed relative to the distance between mouse and edge
-        // the closer to the edge, the faster
-        this._scrollInterval = setInterval(() => {
-          docEl.scrollTop -= speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED;
-        }, 0);
-      }
-
-      if (distanceFromTop <= DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE) {
-        // refer to bottom edge's comments for more info
-        let speed = map(distanceFromTop, 0, DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE,
-                        DRAG_DROP_MIN_AUTOSCROLL_SPEED, DRAG_DROP_MAX_AUTOSCROLL_SPEED);
-
-        this._scrollInterval = setInterval(() => {
-          docEl.scrollTop += speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED;
-        }, 0);
-      }
-
+      this._autoScroll(event);
       return;
-    };
-
-    let target = event.target;
-
-    // Search target for a markupContainer reference, if not found, walk up
+    }
+
+    // Show the current container as hovered and highlight it.
+    // This requires finding the current MarkupContainer (walking up the DOM).
     while (!target.container) {
       if (target.tagName.toLowerCase() === "body") {
         return;
       }
       target = target.parentNode;
     }
 
     let container = target.container;
@@ -233,16 +184,57 @@ MarkupView.prototype = {
         this._showBoxModel(container.node);
       } else {
         this._hideBoxModel();
       }
     }
     this._showContainerAsHovered(container.node);
   },
 
+  /**
+   * Executed on each mouse-move while a node is being dragged in the view.
+   * Auto-scrolls the view to reveal nodes below the fold to drop the dragged
+   * node in.
+   */
+  _autoScroll: function(event) {
+    let docEl = this.doc.documentElement;
+
+    if (this._autoScrollInterval) {
+      clearInterval(this._autoScrollInterval);
+    }
+
+    // Auto-scroll when the mouse approaches top/bottom edge.
+    let fromBottom = docEl.clientHeight - event.pageY + this.win.scrollY;
+    let fromTop = event.pageY - this.win.scrollY;
+
+    if (fromBottom <= DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE) {
+      // Map our distance from 0-50 to 5-15 range so the speed is kept in a
+      // range not too fast, not too slow.
+      let speed = map(
+        fromBottom,
+        0, DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE,
+        DRAG_DROP_MIN_AUTOSCROLL_SPEED, DRAG_DROP_MAX_AUTOSCROLL_SPEED);
+
+      this._autoScrollInterval = setInterval(() => {
+        docEl.scrollTop -= speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED;
+      }, 0);
+    }
+
+    if (fromTop <= DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE) {
+      let speed = map(
+        fromTop,
+        0, DRAG_DROP_AUTOSCROLL_EDGE_DISTANCE,
+        DRAG_DROP_MIN_AUTOSCROLL_SPEED, DRAG_DROP_MAX_AUTOSCROLL_SPEED);
+
+      this._autoScrollInterval = setInterval(() => {
+        docEl.scrollTop += speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED;
+      }, 0);
+    }
+  },
+
   _onMouseClick: function(event) {
     // From the target passed here, let's find the parent MarkupContainer
     // and ask it if the tooltip should be shown
     let parentNode = event.target;
     let container;
     while (parentNode !== this.doc.body) {
       if (parentNode.container) {
         container = parentNode.container;
@@ -256,18 +248,18 @@ MarkupView.prototype = {
       // and decision to show or not the tooltip
       container._buildEventTooltipContent(event.target, this.tooltip);
     }
   },
 
   _onMouseUp: function() {
     this.indicateDropTarget(null);
     this.indicateDragTarget(null);
-    if (this._scrollInterval) {
-      clearInterval(this._scrollInterval);
+    if (this._autoScrollInterval) {
+      clearInterval(this._autoScrollInterval);
     }
   },
 
   cancelDragging: function() {
     if (!this.isDragging) {
       return;
     }
 
@@ -275,23 +267,21 @@ MarkupView.prototype = {
       if (container.isDragging) {
         container.cancelDragging();
         break;
       }
     }
 
     this.indicateDropTarget(null);
     this.indicateDragTarget(null);
-    if (this._scrollInterval) {
-      clearInterval(this._scrollInterval);
+    if (this._autoScrollInterval) {
+      clearInterval(this._autoScrollInterval);
     }
   },
 
-
-
   _hoveredNode: null,
 
   /**
    * Show a NodeFront's container as being hovered
    * @param {NodeFront} nodeFront The node to show as hovered
    */
   _showContainerAsHovered: function(nodeFront) {
     if (this._hoveredNode === nodeFront) {
@@ -302,20 +292,22 @@ MarkupView.prototype = {
       this.getContainer(this._hoveredNode).hovered = false;
     }
 
     this.getContainer(nodeFront).hovered = true;
     this._hoveredNode = nodeFront;
   },
 
   _onMouseLeave: function() {
-    if (this._scrollInterval) {
-      clearInterval(this._scrollInterval);
+    if (this._autoScrollInterval) {
+      clearInterval(this._autoScrollInterval);
     }
-    if (this.isDragging) return;
+    if (this.isDragging) {
+      return;
+    }
 
     this._hideBoxModel(true);
     if (this._hoveredNode) {
       this.getContainer(this._hoveredNode).hovered = false;
     }
     this._hoveredNode = null;
   },
 
@@ -452,17 +444,16 @@ MarkupView.prototype = {
 
   /**
    * React to new-node-front selection events.
    * Highlights the node if needed, and make sure it is shown and selected in
    * the view.
    */
   _onNewSelection: function() {
     let selection = this._inspector.selection;
-    let reason = selection.reason;
 
     this.htmlEditor.hide();
     if (this._hoveredNode && this._hoveredNode !== selection.nodeFront) {
       this.getContainer(this._hoveredNode).hovered = false;
       this._hoveredNode = null;
     }
 
     if (!selection.isNode()) {
@@ -1232,17 +1223,17 @@ MarkupView.prototype = {
       let container = this.getContainer(aNode);
       if (!container) {
         return;
       }
       this.htmlEditor.show(container.tagLine, oldValue);
       this.htmlEditor.once("popuphidden", (e, aCommit, aValue) => {
         // Need to focus the <html> element instead of the frame / window
         // in order to give keyboard focus back to doc (from editor).
-        this._frame.contentDocument.documentElement.focus();
+        this.doc.documentElement.focus();
 
         if (aCommit) {
           this.updateNodeOuterHTML(aNode, aValue, oldValue);
         }
       });
     });
   },
 
@@ -1522,70 +1513,42 @@ MarkupView.prototype = {
     if (this._destroyer) {
       return this._destroyer;
     }
 
     this._destroyer = promise.resolve();
 
     this._clearBriefBoxModelTimer();
 
-    this._elt.removeEventListener("click", this._onMouseClick, false);
-
     this._hoveredNode = null;
-    this._inspector.toolbox.off("picker-node-hovered", this._onToolboxPickerHover);
 
     this.htmlEditor.destroy();
     this.htmlEditor = null;
 
     this.undo.destroy();
     this.undo = null;
 
     this.popup.destroy();
     this.popup = null;
 
-    this._frame.removeEventListener("focus", this._boundFocus, false);
-    this._boundFocus = null;
-
-    if (this._boundUpdatePreview) {
-      this._frame.contentWindow.removeEventListener("scroll",
-        this._boundUpdatePreview, true);
-      this._boundUpdatePreview = null;
-    }
-
-    if (this._boundResizePreview) {
-      this._frame.contentWindow.removeEventListener("resize",
-        this._boundResizePreview, true);
-      this._frame.contentWindow.removeEventListener("overflow",
-        this._boundResizePreview, true);
-      this._frame.contentWindow.removeEventListener("underflow",
-        this._boundResizePreview, true);
-      this._boundResizePreview = null;
-    }
-
-    this._frame.contentWindow.removeEventListener("keydown",
-      this._boundKeyDown, false);
-    this._boundKeyDown = null;
-
-    this._frame.contentWindow.removeEventListener("copy", this._onCopy);
-    this._onCopy = null;
-
-    this._inspector.selection.off("new-node-front", this._boundOnNewSelection);
-    this._boundOnNewSelection = null;
-
-    this.walker.off("mutations", this._boundMutationObserver);
-    this._boundMutationObserver = null;
-
-    this.walker.off("display-change", this._boundOnDisplayChange);
-    this._boundOnDisplayChange = null;
-
+    this._elt.removeEventListener("click", this._onMouseClick, false);
     this._elt.removeEventListener("mousemove", this._onMouseMove, false);
     this._elt.removeEventListener("mouseleave", this._onMouseLeave, false);
+    this.doc.body.removeEventListener("mouseup", this._onMouseUp);
+    this.win.removeEventListener("keydown", this._onKeyDown, false);
+    this.win.removeEventListener("copy", this._onCopy);
+    this._frame.removeEventListener("focus", this._onFocus, false);
+    this.walker.off("mutations", this._mutationObserver);
+    this.walker.off("display-change", this._onDisplayChange);
+    this._inspector.selection.off("new-node-front", this._onNewSelection);
+    this._inspector.toolbox.off("picker-node-hovered", this._onToolboxPickerHover);
+
     this._elt = null;
 
-    for (let [key, container] of this._containers) {
+    for (let [, container] of this._containers) {
       container.destroy();
     }
     this._containers = null;
 
     this.tooltip.destroy();
     this.tooltip = null;
 
     this.win = null;
@@ -1593,55 +1556,69 @@ MarkupView.prototype = {
 
     this._lastDropTarget = null;
     this._lastDragTarget = null;
 
     return this._destroyer;
   },
 
   /**
+   * Find the closest element with class tag-line. These are used to indicate
+   * drag and drop targets.
+   * @param {DOMNode} el
+   * @return {DOMNode}
+   */
+  findClosestDragDropTarget: function(el) {
+    return el.classList.contains("tag-line")
+           ? el
+           : el.querySelector(".tag-line") || el.closest(".tag-line");
+  },
+
+  /**
    * Takes an element as it's only argument and marks the element
    * as the drop target
    */
   indicateDropTarget: function(el) {
     if (this._lastDropTarget) {
       this._lastDropTarget.classList.remove("drop-target");
     }
 
-    if (!el) return;
-
-    let target = el.classList.contains("tag-line") ?
-                 el : el.querySelector(".tag-line") || el.closest(".tag-line");
-    if (!target) return;
-
-    target.classList.add("drop-target");
-    this._lastDropTarget = target;
+    if (!el) {
+      return;
+    }
+
+    let target = this.findClosestDragDropTarget(el);
+    if (target) {
+      target.classList.add("drop-target");
+      this._lastDropTarget = target;
+    }
   },
 
   /**
    * Takes an element to mark it as indicator of dragging target's initial place
    */
   indicateDragTarget: function(el) {
     if (this._lastDragTarget) {
       this._lastDragTarget.classList.remove("drag-target");
     }
 
-    if (!el) return;
-
-    let target = el.classList.contains("tag-line") ?
-                 el : el.querySelector(".tag-line") || el.closest(".tag-line");
-
-    if (!target) return;
-
-    target.classList.add("drag-target");
-    this._lastDragTarget = target;
+    if (!el) {
+      return;
+    }
+
+    let target = this.findClosestDragDropTarget(el);
+    if (target) {
+      target.classList.add("drag-target");
+      this._lastDragTarget = target;
+    }
   },
 
   /**
-   * Used to get the nodes required to modify the markup after dragging the element (parent/nextSibling)
+   * Used to get the nodes required to modify the markup after dragging the
+   * element (parent/nextSibling).
    */
   get dropTargetNodes() {
     let target = this._lastDropTarget;
 
     if (!target) {
       return null;
     }
 
@@ -1681,17 +1658,16 @@ MarkupView.prototype = {
  * This should not be instantiated directly, instead use one of:
  *    MarkupReadOnlyContainer
  *    MarkupTextContainer
  *    MarkupElementContainer
  */
 function MarkupContainer() { }
 
 MarkupContainer.prototype = {
-
   /*
    * Initialize the MarkupContainer.  Should be called while one
    * of the other contain classes is instantiated.
    *
    * @param MarkupView markupView
    *        The markup view that owns this container.
    * @param NodeFront node
    *        The node to display.
@@ -1738,19 +1714,19 @@ MarkupContainer.prototype = {
   isPreviewable: function() {
     if (this.node.tagName && !this.node.isPseudoElement) {
       let tagName = this.node.tagName.toLowerCase();
       let srcAttr = this.editor.getAttributeElement("src");
       let isImage = tagName === "img" && srcAttr;
       let isCanvas = tagName === "canvas";
 
       return isImage || isCanvas;
-    } else {
-      return false;
     }
+
+    return false;
   },
 
   /**
    * Show the element has displayed or not
    */
   set isDisplayed(isDisplayed) {
     this.elt.classList.remove("not-displayed");
     if (!isDisplayed) {
@@ -1863,17 +1839,16 @@ MarkupContainer.prototype = {
       this.expander.removeAttribute("open");
     }
   },
 
   parentContainer: function() {
     return this.elt.parentNode ? this.elt.parentNode.container : null;
   },
 
-  _isMouseDown: false,
   _isDragging: false,
   _dragStartY: 0,
 
   set isDragging(isDragging) {
     this._isDragging = isDragging;
     this.markup.isDragging = isDragging;
 
     if (isDragging) {
@@ -1887,122 +1862,124 @@ MarkupContainer.prototype = {
 
   get isDragging() {
     return this._isDragging;
   },
 
   /**
    * Check if element is draggable
    */
-  isDraggable: function(target) {
-    return this._isMouseDown &&
-           this.markup._dragStartEl === target &&
-           !this.node.isPseudoElement &&
+  isDraggable: function() {
+    let tagName = this.node.tagName.toLowerCase();
+
+    return !this.node.isPseudoElement &&
            !this.node.isAnonymous &&
+           !this.node.isDocumentElement &&
+           tagName !== "body" &&
+           tagName !== "head" &&
            this.win.getSelection().isCollapsed &&
            this.node.parentNode().tagName !== null;
   },
 
   _onMouseDown: function(event) {
-    let target = event.target;
-
-    // The "show more nodes" button (already has its onclick).
+    let {target, button, metaKey, ctrlKey} = event;
+    let isLeftClick = button === 0;
+    let isMiddleClick = button === 1;
+    let isMetaClick = isLeftClick && (metaKey || ctrlKey);
+
+    // The "show more nodes" button already has its onclick, so early return.
     if (target.nodeName === "button") {
       return;
     }
 
     // target is the MarkupContainer itself.
-    this._isMouseDown = true;
     this.hovered = false;
     this.markup.navigate(this);
     event.stopPropagation();
 
     // Preventing the default behavior will avoid the body to gain focus on
     // mouseup (through bubbling) when clicking on a non focusable node in the
     // line. So, if the click happened outside of a focusable element, do
     // prevent the default behavior, so that the tagname or textcontent gains
     // focus.
     if (!target.closest(".editor [tabindex]")) {
       event.preventDefault();
     }
 
-    let isMiddleClick = event.button === 1;
-    let isMetaClick = event.button === 0 && (event.metaKey || event.ctrlKey);
-
+    // Follow attribute links if middle or meta click.
     if (isMiddleClick || isMetaClick) {
       let link = target.dataset.link;
       let type = target.dataset.type;
       this.markup._inspector.followAttributeLink(type, link);
       return;
     }
 
-    // Start dragging the container after a delay.
-    this.markup._dragStartEl = target;
-    setTimeout(() => {
-      // Make sure the mouse is still down and on target.
-      if (!this.isDraggable(target)) {
-        return;
-      }
-      this.isDragging = true;
-
+    // Start node drag & drop (if the mouse moved, see _onMouseMove).
+    if (isLeftClick && this.isDraggable()) {
+      this._isPreDragging = true;
       this._dragStartY = event.pageY;
-      this.markup.indicateDropTarget(this.elt);
-
-      // If this is the last child, use the closing <div.tag-line> of parent as indicator
-      this.markup.indicateDragTarget(this.elt.nextElementSibling ||
-                                     this.markup.getContainer(this.node.parentNode()).closeTagLine);
-    }, this.markup.GRAB_DELAY);
+    }
   },
 
   /**
    * On mouse up, stop dragging.
    */
   _onMouseUp: Task.async(function*() {
-    this._isMouseDown = false;
-
-    if (!this.isDragging) {
-      return;
+    this._isPreDragging = false;
+
+    if (this.isDragging) {
+      this.cancelDragging();
+
+      let dropTargetNodes = this.markup.dropTargetNodes;
+
+      if (!dropTargetNodes) {
+        return;
+      }
+
+      yield this.markup.walker.insertBefore(this.node, dropTargetNodes.parent,
+                                            dropTargetNodes.nextSibling);
+      this.markup.emit("drop-completed");
     }
-
-    this.cancelDragging();
-
-    let dropTargetNodes = this.markup.dropTargetNodes;
-
-    if (!dropTargetNodes) {
-      return;
-    }
-
-    yield this.markup.walker.insertBefore(this.node, dropTargetNodes.parent,
-                                          dropTargetNodes.nextSibling);
-    this.markup.emit("drop-completed");
   }),
 
   /**
-   * On mouse move, move the dragged element if any and indicate the drop target.
+   * On mouse move, move the dragged element and indicate the drop target.
    */
   _onMouseMove: function(event) {
-    if (!this.isDragging) {
-      return;
+    // If this is the first move after mousedown, only start dragging after the
+    // mouse has travelled a few pixels and then indicate the start position.
+    let initialDiff = Math.abs(event.pageY - this._dragStartY);
+    if (this._isPreDragging && initialDiff >= DRAG_DROP_MIN_INITIAL_DISTANCE) {
+      this._isPreDragging = false;
+      this.isDragging = true;
+
+      // If this is the last child, use the closing <div.tag-line> of parent as
+      // indicator.
+      let position = this.elt.nextElementSibling ||
+                     this.markup.getContainer(this.node.parentNode())
+                                .closeTagLine;
+      this.markup.indicateDragTarget(position);
     }
 
-    let diff = event.pageY - this._dragStartY;
-    this.elt.style.top = diff + "px";
-
-    let el = this.markup.doc.elementFromPoint(event.pageX - this.win.scrollX,
-                                              event.pageY - this.win.scrollY);
-
-    this.markup.indicateDropTarget(el);
+    if (this.isDragging) {
+      let diff = event.pageY - this._dragStartY;
+      this.elt.style.top = diff + "px";
+
+      let el = this.markup.doc.elementFromPoint(event.pageX - this.win.scrollX,
+                                                event.pageY - this.win.scrollY);
+      this.markup.indicateDropTarget(el);
+    }
   },
 
   cancelDragging: function() {
     if (!this.isDragging) {
       return;
     }
 
-    this._isMouseDown = false;
+    this._isPreDragging = false;
     this.isDragging = false;
     this.elt.style.removeProperty("top");
   },
 
   /**
    * Temporarily flash the container to attract attention.
    * Used for markup mutations.
    */
--- a/devtools/client/markupview/test/browser.ini
+++ b/devtools/client/markupview/test/browser.ini
@@ -41,22 +41,21 @@ support-files =
 [browser_markupview_anonymous_01.js]
 [browser_markupview_anonymous_02.js]
 skip-if = e10s # scratchpad.xul is not loading in e10s window
 [browser_markupview_anonymous_03.js]
 [browser_markupview_anonymous_04.js]
 [browser_markupview_copy_image_data.js]
 [browser_markupview_css_completion_style_attribute.js]
 [browser_markupview_dragdrop_autoscroll.js]
+[browser_markupview_dragdrop_distance.js]
 [browser_markupview_dragdrop_dragRootNode.js]
 [browser_markupview_dragdrop_escapeKeyPress.js]
 [browser_markupview_dragdrop_invalidNodes.js]
-[browser_markupview_dragdrop_isDragging.js]
 [browser_markupview_dragdrop_reorder.js]
-[browser_markupview_dragdrop_textSelection.js]
 [browser_markupview_events.js]
 skip-if = e10s # Bug 1040751 - CodeMirror editor.destroy() isn't e10s compatible
 [browser_markupview_events_form.js]
 # [browser_markupview_events-overflow.js]
 # skip-if = e10s # Bug 1040751 - CodeMirror editor.destroy() isn't e10s compatible
 # disabled - See bug 1177550
 [browser_markupview_events_jquery_1.0.js]
 skip-if = e10s # Bug 1040751 - CodeMirror editor.destroy() isn't e10s compatible
--- a/devtools/client/markupview/test/browser_markupview_dragdrop_autoscroll.js
+++ b/devtools/client/markupview/test/browser_markupview_dragdrop_autoscroll.js
@@ -1,65 +1,67 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-// Test: Dragging nodes near top/bottom edges of inspector
-// should auto-scroll
+// Test that dragging a node near the top or bottom edge of the markup-view
+// auto-scrolls the view.
 
 const TEST_URL = TEST_URL_ROOT + "doc_markup_dragdrop_autoscroll.html";
-const GRAB_DELAY = 400;
 
 add_task(function*() {
   let {inspector} = yield addTab(TEST_URL).then(openInspector);
-
   let markup = inspector.markup;
+  let viewHeight = markup.doc.documentElement.clientHeight;
 
-  let container = yield getContainerForSelector("#first", inspector);
-  let rect = container.elt.getBoundingClientRect();
+  info("Pretend the markup-view is dragging");
+  markup.isDragging = true;
+
+  info("Simulate a mousemove on the view, at the bottom, and expect scrolling");
+  let onScrolled = waitForViewScroll(markup);
 
-  info("Simulating mouseDown on #first");
-  container._onMouseDown({
-    target: container.tagLine,
-    pageX: 10,
-    pageY: rect.top,
-    stopPropagation: function() {},
-    preventDefault: function() {}
+  markup._onMouseMove({
+    preventDefault: () => {},
+    target: markup.doc.body,
+    pageY: viewHeight
+  });
+
+  let bottomScrollPos = yield onScrolled;
+  ok(bottomScrollPos > 0, "The view was scrolled down");
+
+  info("Simulate a mousemove at the top and expect more scrolling");
+  onScrolled = waitForViewScroll(markup);
+
+  markup._onMouseMove({
+    preventDefault: () => {},
+    target: markup.doc.body,
+    pageY: 0
   });
 
-  yield wait(GRAB_DELAY + 1);
-
-  let clientHeight = markup.doc.documentElement.clientHeight;
-  info("Simulating mouseMove on #first with pageY: " + clientHeight);
-
-  let ev = {
-    target: container.tagLine,
-    pageX: 10,
-    pageY: clientHeight,
-    preventDefault: function() {}
-  };
+  let topScrollPos = yield onScrolled;
+  ok(topScrollPos < bottomScrollPos, "The view was scrolled up");
+  is(topScrollPos, 0, "The view was scrolled up to the top");
 
-  info("Listening on scroll event");
-  let scroll = onScroll(markup.win);
-
-  markup._onMouseMove(ev);
-
-  yield scroll;
-
-  let dropCompleted = once(markup, "drop-completed");
-
-  container._onMouseUp(ev);
-  markup._onMouseUp(ev);
-
-  yield dropCompleted;
-
-  ok("Scroll event fired");
+  info("Simulate a mouseup to stop dragging");
+  markup._onMouseUp();
 });
 
-function onScroll(win) {
-  return new Promise((resolve, reject) => {
-    win.onscroll = function(e) {
-      resolve(e);
-    }
+function waitForViewScroll(markup) {
+  let el = markup.doc.documentElement;
+  let startPos = el.scrollTop;
+
+  return new Promise(resolve => {
+    let isDone = () => {
+      if (el.scrollTop === startPos) {
+        resolve(el.scrollTop);
+      } else {
+        startPos = el.scrollTop;
+        // Continue checking every 50ms.
+        setTimeout(isDone, 50);
+      }
+    };
+
+    // Start checking if the view scrolled after a while.
+    setTimeout(isDone, 50);
   });
-};
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/markupview/test/browser_markupview_dragdrop_distance.js
@@ -0,0 +1,49 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that nodes don't start dragging before the mouse has moved by at least
+// the minimum vertical distance defined in markup-view.js by
+// DRAG_DROP_MIN_INITIAL_DISTANCE.
+
+const TEST_URL = TEST_URL_ROOT + "doc_markup_dragdrop.html";
+const TEST_NODE = "#test";
+
+// Keep this in sync with DRAG_DROP_MIN_INITIAL_DISTANCE in markup-view.js
+const MIN_DISTANCE = 10;
+
+add_task(function*() {
+  let {inspector} = yield addTab(TEST_URL).then(openInspector);
+
+  info("Drag the test node by half of the minimum distance");
+  yield simulateNodeDrag(inspector, TEST_NODE, 0, MIN_DISTANCE / 2);
+  yield checkIsDragging(inspector, TEST_NODE, false);
+
+  info("Drag the test node by exactly the minimum distance");
+  yield simulateNodeDrag(inspector, TEST_NODE, 0, MIN_DISTANCE);
+  yield checkIsDragging(inspector, TEST_NODE, true);
+  inspector.markup.cancelDragging();
+
+  info("Drag the test node by more than the minimum distance");
+  yield simulateNodeDrag(inspector, TEST_NODE, 0, MIN_DISTANCE * 2);
+  yield checkIsDragging(inspector, TEST_NODE, true);
+  inspector.markup.cancelDragging();
+
+  info("Drag the test node by minus the minimum distance");
+  yield simulateNodeDrag(inspector, TEST_NODE, 0, MIN_DISTANCE * -1);
+  yield checkIsDragging(inspector, TEST_NODE, true);
+  inspector.markup.cancelDragging();
+});
+
+function* checkIsDragging(inspector, selector, isDragging) {
+  let container = yield getContainerForSelector(selector, inspector);
+  if (isDragging) {
+    ok(container.isDragging, "The container is being dragged");
+    ok(inspector.markup.isDragging, "And the markup-view knows it");
+  } else {
+    ok(!container.isDragging, "The container hasn't been marked as dragging");
+    ok(!inspector.markup.isDragging, "And the markup-view either");
+  }
+}
--- a/devtools/client/markupview/test/browser_markupview_dragdrop_dragRootNode.js
+++ b/devtools/client/markupview/test/browser_markupview_dragdrop_dragRootNode.js
@@ -1,30 +1,22 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-// Test if html root node is draggable
+// Test that the root node isn't draggable (as well as head and body).
 
 const TEST_URL = TEST_URL_ROOT + "doc_markup_dragdrop.html";
-const GRAB_DELAY = 400;
+const TEST_DATA = ["html", "head", "body"];
 
 add_task(function*() {
   let {inspector} = yield addTab(TEST_URL).then(openInspector);
 
-  let el = yield getContainerForSelector("html", inspector);
-  let rect = el.tagLine.getBoundingClientRect();
+  for (let selector of TEST_DATA) {
+    info("Try to drag/drop node " + selector);
+    yield simulateNodeDrag(inspector, selector);
 
-  info("Simulating mouseDown on html root node");
-  el._onMouseDown({
-    target: el.tagLine,
-    pageX: rect.x,
-    pageY: rect.y,
-    stopPropagation: function() {},
-    preventDefault: function() {}
-  });
-
-  info("Waiting for a little bit more than the markup-view grab delay");
-  yield wait(GRAB_DELAY + 1);
-  is(el.isDragging, false, "isDragging is false");
+    let container = yield getContainerForSelector(selector, inspector);
+    ok(!container.isDragging, "The container hasn't been marked as dragging");
+  }
 });
--- a/devtools/client/markupview/test/browser_markupview_dragdrop_escapeKeyPress.js
+++ b/devtools/client/markupview/test/browser_markupview_dragdrop_escapeKeyPress.js
@@ -1,34 +1,33 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-// Test whether ESCAPE keypress cancels dragging of an element
+// Test whether ESCAPE keypress cancels dragging of an element.
 
 const TEST_URL = TEST_URL_ROOT + "doc_markup_dragdrop.html";
-const GRAB_DELAY = 400;
 
 add_task(function*() {
   let {inspector} = yield addTab(TEST_URL).then(openInspector);
+  let {markup} = inspector;
 
-  let el = yield getContainerForSelector("#test", inspector);
-  let rect = el.tagLine.getBoundingClientRect();
+  info("Get a test container");
+  let container = yield getContainerForSelector("#test", inspector);
+
+  info("Simulate a drag/drop on this container");
+  yield simulateNodeDrag(inspector, "#test");
 
-  info("Simulating mouseDown on #test");
-  el._onMouseDown({
-    target: el.tagLine,
-    pageX: rect.x,
-    pageY: rect.y,
-    stopPropagation: function() {},
-    preventDefault: function() {}
-  });
+  ok(container.isDragging && markup.isDragging,
+     "The container is being dragged");
+  ok(markup.doc.body.classList.contains("dragging"),
+     "The dragging css class was added");
 
-  info("Waiting for a little bit more than the markup-view grab delay");
-  yield wait(GRAB_DELAY + 1);
-  ok(el.isDragging, "isDragging true after mouseDown");
+  info("Simulate ESCAPE keypress");
+  EventUtils.sendKey("escape", inspector.panelWin);
 
-  info("Simulating ESCAPE keypress");
-  EventUtils.sendKey("escape", inspector.panelWin);
-  is(el.isDragging, false, "isDragging false after ESCAPE keypress");
+  ok(!container.isDragging && !markup.isDragging,
+     "The dragging has stopped");
+  ok(!markup.doc.body.classList.contains("dragging"),
+     "The dragging css class was removed");
 });
--- a/devtools/client/markupview/test/browser_markupview_dragdrop_invalidNodes.js
+++ b/devtools/client/markupview/test/browser_markupview_dragdrop_invalidNodes.js
@@ -1,58 +1,45 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-// Test: pseudo-elements and anonymous nodes should not be draggable
+// Check that pseudo-elements and anonymous nodes are not draggable.
 
 const TEST_URL = TEST_URL_ROOT + "doc_markup_dragdrop.html";
-const GRAB_DELAY = 400;
 
 add_task(function*() {
   Services.prefs.setBoolPref("devtools.inspector.showAllAnonymousContent", true);
 
   let {inspector} = yield addTab(TEST_URL).then(openInspector);
 
-  info("Expanding #test");
+  info("Expanding nodes below #test");
   let parentFront = yield getNodeFront("#test", inspector);
   yield inspector.markup.expandNode(parentFront);
   yield waitForMultipleChildrenUpdates(inspector);
 
+  info("Getting the ::before pseudo element");
   let parentContainer = yield getContainerForNodeFront(parentFront, inspector);
   let beforePseudo = parentContainer.elt.children[1].firstChild.container;
-
   parentContainer.elt.scrollIntoView(true);
 
-  info("Simulating mouseDown on #test::before");
-  beforePseudo._onMouseDown({
-    target: beforePseudo.tagLine,
-    stopPropagation: function() {},
-    preventDefault: function() {}
-  });
+  info("Simulate dragging the ::before pseudo element");
+  yield simulateNodeDrag(inspector, beforePseudo);
 
-  info("Waiting " + (GRAB_DELAY + 1) + "ms")
-  yield wait(GRAB_DELAY + 1);
-  is(beforePseudo.isDragging, false, "[pseudo-element] isDragging is false after GRAB_DELAY has passed");
+  ok(!beforePseudo.isDragging, "::before pseudo element isn't dragging");
 
+  info("Expanding nodes below #anonymousParent");
   let inputFront = yield getNodeFront("#anonymousParent", inspector);
-
   yield inspector.markup.expandNode(inputFront);
   yield waitForMultipleChildrenUpdates(inspector);
 
+  info("Getting the anonymous node");
   let inputContainer = yield getContainerForNodeFront(inputFront, inspector);
   let anonymousDiv = inputContainer.elt.children[1].firstChild.container;
-
   inputContainer.elt.scrollIntoView(true);
 
-  info("Simulating mouseDown on input#anonymousParent div");
-  anonymousDiv._onMouseDown({
-    target: anonymousDiv.tagLine,
-    stopPropagation: function() {},
-    preventDefault: function() {}
-  });
+  info("Simulate dragging the anonymous node");
+  yield simulateNodeDrag(inspector, anonymousDiv);
 
-  info("Waiting " + (GRAB_DELAY + 1) + "ms")
-  yield wait(GRAB_DELAY + 1);
-  is(anonymousDiv.isDragging, false, "[anonymous element] isDragging is false after GRAB_DELAY has passed");
+  ok(!anonymousDiv.isDragging, "anonymous node isn't dragging");
 });
deleted file mode 100644
--- a/devtools/client/markupview/test/browser_markupview_dragdrop_isDragging.js
+++ /dev/null
@@ -1,65 +0,0 @@
-/* vim: set ts=2 et sw=2 tw=80: */
-/* Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-// Test drag mode's delay, it shouldn't enable dragging before
-// GRAB_DELAY = 400 has passed
-
-const TEST_URL = TEST_URL_ROOT + "doc_markup_dragdrop.html";
-const GRAB_DELAY = 400;
-
-add_task(function*() {
-  let {inspector} = yield addTab(TEST_URL).then(openInspector);
-
-  let el = yield getContainerForSelector("#test", inspector);
-  let rect = el.tagLine.getBoundingClientRect();
-
-  info("Simulating mouseDown on #test");
-  el._onMouseDown({
-    target: el.tagLine,
-    pageX: rect.x,
-    pageY: rect.y,
-    stopPropagation: function() {},
-    preventDefault: function() {}
-  });
-
-  ok(!el.isDragging, "isDragging should not be set to true immediately");
-
-  info("Waiting for 10ms");
-  yield wait(10);
-  ok(!el.isDragging, "isDragging should not be set to true after a brief wait");
-
-  info("Waiting " + (GRAB_DELAY + 1) + "ms");
-  yield wait(GRAB_DELAY + 1);
-  ok(el.isDragging, "isDragging true after GRAB_DELAY has passed");
-
-  let dropCompleted = once(inspector.markup, "drop-completed");
-
-  info("Simulating mouseUp on #test");
-  el._onMouseUp({
-    target: el.tagLine,
-    pageX: rect.x,
-    pageY: rect.y
-  });
-
-  yield dropCompleted;
-
-  ok(!el.isDragging, "isDragging false after mouseUp");
-
-  info("Simulating middle click on #test");
-  el._onMouseDown({
-    target: el.tagLine,
-    button: 1,
-    pageX: rect.x,
-    pageY: rect.y,
-    stopPropagation: function() {},
-    preventDefault: function() {}
-  });
-  ok(!el.isDragging, "isDragging should not be set to true immediately");
-
-  info("Waiting " + (GRAB_DELAY + 1) + "ms");
-  yield wait(GRAB_DELAY + 1);
-  ok(!el.isDragging, "isDragging never starts after middle click after mouseUp");
-});
--- a/devtools/client/markupview/test/browser_markupview_dragdrop_reorder.js
+++ b/devtools/client/markupview/test/browser_markupview_dragdrop_reorder.js
@@ -1,138 +1,107 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-// Test different kinds of drag and drop node re-ordering
+// Test different kinds of drag and drop node re-ordering.
 
 const TEST_URL = TEST_URL_ROOT + "doc_markup_dragdrop.html";
-const GRAB_DELAY = 5;
 
 add_task(function*() {
   let {inspector} = yield addTab(TEST_URL).then(openInspector);
-  inspector.markup.GRAB_DELAY = GRAB_DELAY;
+  let ids;
 
-  info("Expanding #test");
+  info("Expand #test node");
   let parentFront = yield getNodeFront("#test", inspector);
-  let parent = yield getNode("#test");
-  let parentContainer = yield getContainerForNodeFront(parentFront, inspector);
-
   yield inspector.markup.expandNode(parentFront);
   yield waitForMultipleChildrenUpdates(inspector);
 
+  info("Scroll #test into view");
+  let parentContainer = yield getContainerForNodeFront(parentFront, inspector);
   parentContainer.elt.scrollIntoView(true);
 
-  info("Testing putting an element back in it's original place");
+  info("Test putting an element back at its original place");
   yield dragElementToOriginalLocation("#firstChild", inspector);
-  is(parent.children[0].id, "firstChild", "#firstChild is still the first child of #test");
-  is(parent.children[1].id, "middleChild", "#middleChild is still the second child of #test");
+  ids = yield getChildrenIDsOf(parentFront, inspector);
+  is(ids[0], "firstChild",
+     "#firstChild is still the first child of #test");
+  is(ids[1], "middleChild",
+     "#middleChild is still the second child of #test");
 
   info("Testing switching elements inside their parent");
   yield moveElementDown("#firstChild", "#middleChild", inspector);
-
-  is(parent.children[0].id, "middleChild", "#firstChild is now the second child of #test");
-  is(parent.children[1].id, "firstChild", "#middleChild is now the first child of #test");
+  ids = yield getChildrenIDsOf(parentFront, inspector);
+  is(ids[0], "middleChild",
+     "#firstChild is now the second child of #test");
+  is(ids[1], "firstChild",
+     "#middleChild is now the first child of #test");
 
   info("Testing switching elements with a last child");
   yield moveElementDown("#firstChild", "#lastChild", inspector);
-
-  is(parent.children[1].id, "lastChild", "#lastChild is now the second child of #test");
-  is(parent.children[2].id, "firstChild", "#firstChild is now the last child of #test");
+  ids = yield getChildrenIDsOf(parentFront, inspector);
+  is(ids[1], "lastChild",
+     "#lastChild is now the second child of #test");
+  is(ids[2], "firstChild",
+     "#firstChild is now the last child of #test");
 
   info("Testing appending element to a parent");
   yield moveElementDown("#before", "#test", inspector);
-
-  is(parent.children.length, 4, "New element appended to #test");
-  is(parent.children[0].id, "before", "New element is appended at the right place (currently first child)");
+  ids = yield getChildrenIDsOf(parentFront, inspector);
+  is(ids.length, 4,
+     "New element appended to #test");
+  is(ids[0], "before",
+     "New element is appended at the right place (currently first child)");
 
   info("Testing moving element to after it's parent");
   yield moveElementDown("#firstChild", "#test", inspector);
-
-  is(parent.children.length, 3, "#firstChild is no longer #test's child");
-  is(parent.nextElementSibling.id, "firstChild", "#firstChild is now #test's nextElementSibling");
+  ids = yield getChildrenIDsOf(parentFront, inspector);
+  is(ids.length, 3,
+     "#firstChild is no longer #test's child");
+  let siblingFront = yield inspector.walker.nextSibling(parentFront);
+  is(siblingFront.id, "firstChild",
+     "#firstChild is now #test's nextElementSibling");
 });
 
-function* dragContainer(selector, targetOffset, inspector) {
-  info("Dragging the markup-container for node " + selector);
-
-  let container = yield getContainerForSelector(selector, inspector);
-
-  let updated = inspector.once("inspector-updated");
-
-  let rect = {
-    x: container.tagLine.offsetLeft,
-    y: container.tagLine.offsetTop
-  };
-
-  info("Simulating mouseDown on " + selector);
-  container._onMouseDown({
-    target: container.tagLine,
-    pageX: rect.x,
-    pageY: rect.y,
-    stopPropagation: function() {},
-    preventDefault: function() {}
-  });
-
-  let targetX = rect.x + targetOffset.x,
-      targetY = rect.y + targetOffset.y;
-
-  setTimeout(() => {
-    info("Simulating mouseMove on " + selector +
-         " with pageX: " + targetX + " pageY: " + targetY);
-    container._onMouseMove({
-      target: container.tagLine,
-      pageX: targetX,
-      pageY: targetY
-    });
-
-    info("Simulating mouseUp on " + selector +
-         " with pageX: " + targetX + " pageY: " + targetY);
-    container._onMouseUp({
-      target: container.tagLine,
-      pageX: targetX,
-      pageY: targetY
-    });
-
-    container.markup._onMouseUp();
-  }, GRAB_DELAY+1);
-
-  return updated;
-};
-
 function* dragElementToOriginalLocation(selector, inspector) {
-  let el = yield getContainerForSelector(selector, inspector);
-  let height = el.tagLine.getBoundingClientRect().height;
-
   info("Picking up and putting back down " + selector);
 
   function onMutation() {
     ok(false, "Mutation received from dragging a node back to its location");
   }
   inspector.on("markupmutation", onMutation);
-  yield dragContainer(selector, {x: 0, y: 0}, inspector);
+  yield simulateNodeDragAndDrop(inspector, selector, 0, 0);
 
   // Wait a bit to make sure the event never fires.
   // This doesn't need to catch *all* cases, since the mutation
   // will cause failure later in the test when it checks element ordering.
-  yield new Promise(resolve => {
-    setTimeout(resolve, 500);
-  });
+  yield wait(500);
   inspector.off("markupmutation", onMutation);
 }
 
 function* moveElementDown(selector, next, inspector) {
+  info("Switching " + selector + " with " + next);
+
+  let container = yield getContainerForSelector(next, inspector);
+  let height = container.tagLine.getBoundingClientRect().height;
+
   let onMutated = inspector.once("markupmutation");
   let uiUpdate = inspector.once("inspector-updated");
 
-  let el = yield getContainerForSelector(next, inspector);
-  let height = el.tagLine.getBoundingClientRect().height;
-
-  info("Switching " + selector + ' with ' + next);
-
-  yield dragContainer(selector, {x: 0, y: Math.round(height) + 2}, inspector);
+  yield simulateNodeDragAndDrop(inspector, selector, 0, Math.round(height) + 2);
 
   let mutations = yield onMutated;
-  is(mutations.length, 2, "2 mutations");
   yield uiUpdate;
-};
\ No newline at end of file
+
+  is(mutations.length, 2, "2 mutations were received");
+}
+
+function* getChildrenIDsOf(parentFront, {walker}) {
+  let {nodes} = yield walker.children(parentFront);
+  // Filter out non-element nodes since children also returns pseudo-elements.
+  return nodes.filter(node => {
+    return !node.isPseudoElement;
+  }).map(node => {
+    return node.id;
+  });
+}
deleted file mode 100644
--- a/devtools/client/markupview/test/browser_markupview_dragdrop_textSelection.js
+++ /dev/null
@@ -1,48 +0,0 @@
-/* vim: set ts=2 et sw=2 tw=80: */
-/* Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-// Test: Nodes should not be draggable if there is a text selected
-// (trying to move selected text around shouldn't trigger node drag and drop)
-
-const TEST_URL = TEST_URL_ROOT + "doc_markup_dragdrop.html";
-const GRAB_DELAY = 400;
-
-add_task(function*() {
-  let {inspector} = yield addTab(TEST_URL).then(openInspector);
-  let markup = inspector.markup;
-
-  info("Expanding span#before");
-  let spanFront = yield getNodeFront("#before", inspector);
-  let spanContainer = yield getContainerForNodeFront(spanFront, inspector);
-  let span = yield getNode("#before");
-
-  yield inspector.markup.expandNode(spanFront);
-  yield waitForMultipleChildrenUpdates(inspector);
-
-  spanContainer.elt.scrollIntoView(true);
-
-  info("Selecting #before's text content");
-
-  let textContent = spanContainer.elt.children[1].firstChild.container;
-
-  let selectRange = markup.doc.createRange();
-  selectRange.selectNode(textContent.editor.elt.querySelector('[tabindex]'));
-  markup.doc.getSelection().addRange(selectRange);
-
-  info("Simulating mouseDown on #before");
-
-  spanContainer._onMouseDown({
-    pageX: 0,
-    pageY: 0,
-    target: spanContainer.tagLine,
-    stopPropagation: function() {},
-    preventDefault: function() {}
-  });
-
-  yield wait(GRAB_DELAY + 1);
-
-  is(spanContainer.isDragging, false, "isDragging should be false if there is a text selected");
-});
\ No newline at end of file
--- a/devtools/client/markupview/test/head.js
+++ b/devtools/client/markupview/test/head.js
@@ -771,8 +771,78 @@ function registerTabActor(client, option
  * @returns A promise that is resolved when the unregistration
  * has finished.
  */
 function unregisterActor(registrar, front) {
   return front.detach().then(() => {
     return registrar.unregister();
   });
 }
+
+/**
+ * Simulate dragging a MarkupContainer by calling its mousedown and mousemove
+ * handlers.
+ * @param {InspectorPanel} inspector The current inspector-panel instance.
+ * @param {String|MarkupContainer} selector The selector to identify the node or
+ * the MarkupContainer for this node.
+ * @param {Number} xOffset Optional x offset to drag by.
+ * @param {Number} yOffset Optional y offset to drag by.
+ */
+function* simulateNodeDrag(inspector, selector, xOffset = 10, yOffset = 10) {
+  let container = typeof selector === "string"
+                  ? yield getContainerForSelector(selector, inspector)
+                  : selector;
+  let rect = container.tagLine.getBoundingClientRect();
+  let scrollX = inspector.markup.doc.documentElement.scrollLeft;
+  let scrollY = inspector.markup.doc.documentElement.scrollTop;
+
+  info("Simulate mouseDown on element " + selector);
+  container._onMouseDown({
+    target: container.tagLine,
+    button: 0,
+    pageX: scrollX + rect.x,
+    pageY: scrollY + rect.y,
+    stopPropagation: () => {},
+    preventDefault: () => {}
+  });
+
+  // _onMouseDown selects the node, so make sure to wait for the
+  // inspector-updated event if the current selection was different.
+  if (inspector.selection.nodeFront !== container.node) {
+    yield inspector.once("inspector-updated");
+  }
+
+  info("Simulate mouseMove on element " + selector);
+  container._onMouseMove({
+    pageX: scrollX + rect.x + xOffset,
+    pageY: scrollY + rect.y + yOffset
+  });
+}
+
+/**
+ * Simulate dropping a MarkupContainer by calling its mouseup handler. This is
+ * meant to be called after simulateNodeDrag has been called.
+ * @param {InspectorPanel} inspector The current inspector-panel instance.
+ * @param {String|MarkupContainer} selector The selector to identify the node or
+ * the MarkupContainer for this node.
+ */
+function* simulateNodeDrop(inspector, selector) {
+  info("Simulate mouseUp on element " + selector);
+  let container = typeof selector === "string"
+                  ? yield getContainerForSelector(selector, inspector)
+                  : selector;
+  container._onMouseUp();
+  inspector.markup._onMouseUp();
+}
+
+/**
+ * Simulate drag'n'dropping a MarkupContainer by calling its mousedown,
+ * mousemove and mouseup handlers.
+ * @param {InspectorPanel} inspector The current inspector-panel instance.
+ * @param {String|MarkupContainer} selector The selector to identify the node or
+ * the MarkupContainer for this node.
+ * @param {Number} xOffset Optional x offset to drag by.
+ * @param {Number} yOffset Optional y offset to drag by.
+ */
+function* simulateNodeDragAndDrop(inspector, selector, xOffset, yOffset) {
+  yield simulateNodeDrag(inspector, selector, xOffset, yOffset);
+  yield simulateNodeDrop(inspector, selector);
+}
--- a/devtools/client/memory/components/heap.js
+++ b/devtools/client/memory/components/heap.js
@@ -42,16 +42,19 @@ function createTreeProperties (census) {
 
   return {
     getParent: node => map(node.id),
     getChildren: node => node.children || [],
     renderItem: (item, depth, focused, arrow) => new TreeItem({ item, depth, focused, arrow }),
     getRoots: () => census.children,
     getKey: node => node.id,
     itemHeight: HEAP_TREE_ROW_HEIGHT,
+    // Because we never add or remove children when viewing the same census, we
+    // can always reuse a cached traversal if one is available.
+    reuseCachedTraversal: traversal => true,
   };
 }
 
 /**
  * Main view for the memory tool -- contains several panels for different states;
  * an initial state of only a button to take a snapshot, loading states, and the
  * heap view tree.
  */
--- a/devtools/client/memory/components/toolbar.js
+++ b/devtools/client/memory/components/toolbar.js
@@ -31,20 +31,23 @@ const Toolbar = module.exports = createC
       onToggleInverted,
       inverted
     } = this.props;
 
     return (
       DOM.div({ className: "devtools-toolbar" }, [
         DOM.button({ className: `take-snapshot devtools-button`, onClick: onTakeSnapshotClick }),
 
-        DOM.select({
-          className: `select-breakdown`,
-          onChange: e => onBreakdownChange(e.target.value),
-        }, breakdowns.map(({ name, displayName }) => DOM.option({ value: name }, displayName))),
+        DOM.label({},
+          "Breakdown by ",
+          DOM.select({
+            className: `select-breakdown`,
+            onChange: e => onBreakdownChange(e.target.value),
+          }, breakdowns.map(({ name, displayName }) => DOM.option({ value: name }, displayName)))
+        ),
 
         DOM.label({}, [
           DOM.input({
             type: "checkbox",
             checked: inverted,
             onChange: onToggleInverted,
           }),
           // TODO bug 1214799
--- a/devtools/client/memory/components/tree.js
+++ b/devtools/client/memory/components/tree.js
@@ -95,17 +95,17 @@ const TreeNode = createFactory(createCla
       // margin to completely hide it.
       MozMarginStart: "-1000px !important",
     }
   }
 }));
 
 /**
  * A generic tree component. See propTypes for the public API.
- * 
+ *
  * @see `devtools/client/memory/components/test/mochitest/head.js` for usage
  * @see `devtools/client/memory/components/heap.js` for usage
  */
 const Tree = module.exports = createClass({
   displayName: "Tree",
 
   propTypes: {
     // Required props
@@ -125,36 +125,42 @@ const Tree = module.exports = createClas
     // pixels.
     itemHeight: PropTypes.number.isRequired,
 
     // Optional props
 
     // A predicate function to filter out unwanted items from the tree.
     filter: PropTypes.func,
     // The depth to which we should automatically expand new items.
-    autoExpandDepth: PropTypes.number
+    autoExpandDepth: PropTypes.number,
+    // A predicate that returns true if the last DFS traversal that was cached
+    // can be reused, false otherwise. The predicate function is passed the
+    // cached traversal as an array of nodes.
+    reuseCachedTraversal: PropTypes.func,
   },
 
   getDefaultProps() {
     return {
       filter: item => true,
       expanded: new Set(),
       seen: new Set(),
       focused: undefined,
-      autoExpandDepth: AUTO_EXPAND_DEPTH
+      autoExpandDepth: AUTO_EXPAND_DEPTH,
+      reuseCachedTraversal: null,
     };
   },
 
   getInitialState() {
     return {
       scroll: 0,
       height: window.innerHeight,
       expanded: new Set(),
       seen: new Set(),
-      focused: undefined
+      focused: undefined,
+      cachedTraversal: undefined,
     };
   },
 
   componentDidMount() {
     window.addEventListener("resize", this._updateHeight);
     this._updateHeight();
   },
 
@@ -268,20 +274,33 @@ const Tree = module.exports = createClas
 
     return traversal;
   },
 
   /**
    * Perform a pre-order depth-first search over the whole forest.
    */
   _dfsFromRoots(maxDepth = Infinity) {
+    const cached = this.state.cachedTraversal;
+    if (cached
+        && maxDepth === Infinity
+        && this.props.reuseCachedTraversal
+        && this.props.reuseCachedTraversal(cached)) {
+      return cached;
+    }
+
     const traversal = [];
     for (let root of this.props.getRoots()) {
       this._dfs(root, maxDepth, traversal);
     }
+
+    if (this.props.reuseCachedTraversal) {
+      this.state.cachedTraversal = traversal;
+    }
+
     return traversal;
   },
 
   /**
    * Expands current row.
    *
    * @param {Object} item
    * @param {Boolean} expandAllChildren
@@ -291,29 +310,31 @@ const Tree = module.exports = createClas
 
     if (expandAllChildren) {
       for (let { item: child } of this._dfs(item)) {
         this.state.expanded.add(child);
       }
     }
 
     this.setState({
-      expanded: this.state.expanded
+      expanded: this.state.expanded,
+      cachedTraversal: null,
     });
   },
 
   /**
    * Collapses current row.
    *
    * @param {Object} item
    */
   _onCollapse(item) {
     this.state.expanded.delete(item);
     this.setState({
-      expanded: this.state.expanded
+      expanded: this.state.expanded,
+      cachedTraversal: null,
     });
   },
 
   /**
    * Sets the passed in item to be the focused item.
    *
    * @param {Object} item
    */
--- a/devtools/client/memory/constants.js
+++ b/devtools/client/memory/constants.js
@@ -44,24 +44,24 @@ const ALLOCATION_STACK = { by: "allocati
 const OBJECT_CLASS = { by: "objectClass", then: COUNT, other: COUNT };
 
 const breakdowns = exports.breakdowns = {
   coarseType: {
     displayName: "Coarse Type",
     breakdown: {
       by: "coarseType",
       objects: ALLOCATION_STACK,
-      strings: ALLOCATION_STACK,
+      strings: COUNT,
       scripts: INTERNAL_TYPE,
       other: INTERNAL_TYPE,
     }
   },
 
   allocationStack: {
-    displayName: "Allocation Site",
+    displayName: "Allocation Stack",
     breakdown: ALLOCATION_STACK,
   },
 
   objectClass: {
     displayName: "Object Class",
     breakdown: OBJECT_CLASS,
   },
 
--- a/devtools/client/performance/docs/markers.md
+++ b/devtools/client/performance/docs/markers.md
@@ -2,16 +2,25 @@
 
 ## Common
 
 * DOMHighResTimeStamp start
 * DOMHighResTimeStamp end
 * DOMString name
 * object? stack
 * object? endStack
+* unsigned short processType;
+* boolean isOffMainThread;
+
+The `processType` a GeckoProcessType enum listed in xpcom/build/nsXULAppAPI.h,
+specifying if this marker originates in a content, chrome, plugin etc. process.
+Additionally, markers may be created from any thread on those processes, and
+`isOffMainThread` highights whether or not they're from the main thread. The most
+common type of marker is probably going to be from a GeckoProcessType_Content's
+main thread when debugging content.
 
 ## DOMEvent
 
 Triggered when a DOM event occurs, like a click or a keypress.
 
 * unsigned short eventPhase - a number indicating what phase this event is
   in (target, bubbling, capturing, maps to Ci.nsIDOMEvent constants)
 * DOMString type - the type of event, like "keypress" or "click"
--- a/devtools/client/performance/modules/logic/waterfall-utils.js
+++ b/devtools/client/performance/modules/logic/waterfall-utils.js
@@ -27,55 +27,68 @@ function createParentNode (marker) {
 
 /**
  * Collapses markers into a tree-like structure.
  * @param object rootNode
  * @param array markersList
  * @param array filter
  */
 function collapseMarkersIntoNode({ rootNode, markersList, filter }) {
-  let { getCurrentParentNode, pushNode, popParentNode } = createParentNodeFactory(rootNode);
+  let {
+    getCurrentParentNode,
+    pushNode,
+    popParentNode
+  } = createParentNodeFactory(rootNode);
 
   for (let i = 0, len = markersList.length; i < len; i++) {
     let curr = markersList[i];
 
     // If this marker type should not be displayed, just skip
     if (!MarkerUtils.isMarkerValid(curr, filter)) {
       continue;
     }
 
     let parentNode = getCurrentParentNode();
     let blueprint = MarkerUtils.getBlueprintFor(curr);
 
     let nestable = "nestable" in blueprint ? blueprint.nestable : true;
     let collapsible = "collapsible" in blueprint ? blueprint.collapsible : true;
 
-    let finalized = null;
+    let finalized = false;
 
     // If this marker is collapsible, turn it into a parent marker.
     // If there are no children within it later, it will be turned
     // back into a normal node.
     if (collapsible) {
       curr = createParentNode(curr);
     }
 
-    // If not nestible, just push it inside the root node,
-    // like console.time/timeEnd.
-    if (!nestable) {
+    // If not nestible, just push it inside the root node. Additionally,
+    // markers originating outside the main thread are considered to be
+    // "never collapsible", to avoid confusion.
+    // A beter solution would be to collapse every marker with its siblings
+    // from the same thread, but that would require a thread id attached
+    // to all markers, which is potentially expensive and rather useless at
+    // the moment, since we don't really have that many OTMT markers.
+    if (!nestable || curr.isOffMainThread) {
       pushNode(rootNode, curr);
       continue;
     }
 
     // First off, if any parent nodes exist, finish them off
     // recursively upwards if this marker is outside their ranges and nestable.
     while (!finalized && parentNode) {
       // If this marker is eclipsed by the current parent marker,
-      // make it a child of the current parent and stop
-      // going upwards.
-      if (nestable && curr.end <= parentNode.end) {
+      // make it a child of the current parent and stop going upwards.
+      // If the markers aren't from the same process, attach them to the root
+      // node as well. Every process has its own main thread.
+      if (nestable &&
+          curr.start >= parentNode.start &&
+          curr.end <= parentNode.end &&
+          curr.processType == parentNode.processType) {
         pushNode(parentNode, curr);
         finalized = true;
         break;
       }
 
       // If this marker is still nestable, but outside of the range
       // of the current parent, iterate upwards on the next parent
       // and finalize the current parent.
@@ -107,36 +120,39 @@ function createParentNodeFactory (root) 
      * Sets the `end` time based on the most recent child if not defined.
      */
     popParentNode: () => {
       if (parentMarkers.length === 0) {
         throw new Error("Cannot pop parent markers when none exist.");
       }
 
       let lastParent = parentMarkers.pop();
+
       // If this finished parent marker doesn't have an end time,
       // so probably a synthesized marker, use the last marker's end time.
       if (lastParent.end == void 0) {
         lastParent.end = lastParent.submarkers[lastParent.submarkers.length - 1].end;
       }
 
       // If no children were ever pushed into this parent node,
-      // remove it's submarkers so it behaves like a non collapsible
+      // remove its submarkers so it behaves like a non collapsible
       // node.
       if (!lastParent.submarkers.length) {
         delete lastParent.submarkers;
       }
 
       return lastParent;
     },
 
     /**
      * Returns the most recent parent node.
      */
-    getCurrentParentNode: () => parentMarkers.length ? parentMarkers[parentMarkers.length - 1] : null,
+    getCurrentParentNode: () => parentMarkers.length
+      ? parentMarkers[parentMarkers.length - 1]
+      : null,
 
     /**
      * Push this marker into the most recent parent node.
      */
     pushNode: (parent, marker) => {
       parent.submarkers.push(marker);
 
       // If pushing a parent marker, track it as the top of
--- a/devtools/client/performance/modules/widgets/marker-view.js
+++ b/devtools/client/performance/modules/widgets/marker-view.js
@@ -105,16 +105,17 @@ MarkerView.prototype = Heritage.extend(A
    * Creates the view for this waterfall node.
    * @param nsIDOMNode document
    * @param nsIDOMNode arrowNode
    * @return nsIDOMNode
    */
   _displaySelf: function(document, arrowNode) {
     let targetNode = document.createElement("hbox");
     targetNode.className = "waterfall-tree-item";
+    targetNode.setAttribute("otmt", this.marker.isOffMainThread);
 
     if (this == this.root) {
       // Bounds are needed for properly positioning and scaling markers in
       // the waterfall, but it's sufficient to make those calculations only
       // for the root node.
       this.root.recalculateBounds();
       // The AbstractTreeItem propagates events to the root, so we don't
       // need to listen them on descendant items in the tree.
--- a/devtools/client/performance/test/browser_timeline-waterfall-workers.js
+++ b/devtools/client/performance/test/browser_timeline-waterfall-workers.js
@@ -2,17 +2,17 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests if the sidebar is properly updated with worker markers.
  */
 
 function* spawnTest() {
   let { panel } = yield initPerformance(WORKER_URL);
-  let { PerformanceController } = panel.panelWin;
+  let { $$, $, PerformanceController } = panel.panelWin;
 
   loadFrameScripts();
 
   yield startRecording(panel);
   ok(true, "Recording has started.");
 
   evalInDebuggee("performWork()");
 
@@ -22,36 +22,53 @@ function* spawnTest() {
     if (!markers.some(m => m.name == "Worker") ||
         !markers.some(m => m.workerOperation == "serializeDataOffMainThread") ||
         !markers.some(m => m.workerOperation == "serializeDataOnMainThread") ||
         !markers.some(m => m.workerOperation == "deserializeDataOffMainThread") ||
         !markers.some(m => m.workerOperation == "deserializeDataOnMainThread")) {
       return false;
     }
 
-    testWorkerMarker(markers.find(m => m.name == "Worker"));
+    testWorkerMarkerData(markers.find(m => m.name == "Worker"));
     return true;
   });
 
   yield stopRecording(panel);
   ok(true, "Recording has ended.");
 
+  for (let node of $$(".waterfall-marker-name[value=Worker")) {
+    testWorkerMarkerUI(node.parentNode.parentNode);
+  }
+
   yield teardown(panel);
   finish();
 }
 
-function testWorkerMarker(marker) {
+function testWorkerMarkerData(marker) {
   ok(true, "Found a worker marker.");
 
   ok("start" in marker,
     "The start time is specified in the worker marker.");
   ok("end" in marker,
     "The end time is specified in the worker marker.");
+
   ok("workerOperation" in marker,
     "The worker operation is specified in the worker marker.");
+
+  ok("processType" in marker,
+    "The process type is specified in the worker marker.");
+  ok("isOffMainThread" in marker,
+    "The thread origin is specified in the worker marker.");
+}
+
+function testWorkerMarkerUI(node) {
+  is(node.className, "waterfall-tree-item",
+    "The marker node has the correct class name.");
+  ok(node.hasAttribute("otmt"),
+    "The marker node specifies if it is off the main thread or not.");
 }
 
 /**
  * Takes a string `script` and evaluates it directly in the content
  * in potentially a different process.
  */
 function evalInDebuggee (script) {
   let { generateUUID } = Cc['@mozilla.org/uuid-generator;1'].getService(Ci.nsIUUIDGenerator);
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-05.js
@@ -0,0 +1,163 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the waterfall collapsing logic works properly
+ * when dealing with OTMT markers.
+ */
+
+function run_test() {
+  run_next_test();
+}
+
+add_task(function test() {
+  const WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
+
+  let rootMarkerNode = WaterfallUtils.createParentNode({ name: "(root)" });
+
+  WaterfallUtils.collapseMarkersIntoNode({
+    rootNode: rootMarkerNode,
+    markersList: gTestMarkers
+  });
+
+  compare(rootMarkerNode, gExpectedOutput);
+
+  function compare (marker, expected) {
+    for (let prop in expected) {
+      if (prop === "submarkers") {
+        for (let i = 0; i < expected.submarkers.length; i++) {
+          compare(marker.submarkers[i], expected.submarkers[i]);
+        }
+      } else if (prop !== "uid") {
+        equal(marker[prop], expected[prop], `${expected.name} matches ${prop}`);
+      }
+    }
+  }
+});
+
+const gTestMarkers = [
+  { start: 1, end: 4, name: "A1-mt", processType: 1, isOffMainThread: false },
+  // This should collapse only under A1-mt
+  { start: 2, end: 3, name: "B1", processType: 1, isOffMainThread: false },
+  // This should never collapse.
+  { start: 2, end: 3, name: "C1", processType: 1, isOffMainThread: true },
+
+  { start: 5, end: 8, name: "A1-otmt", processType: 1, isOffMainThread: true },
+  // This should collapse only under A1-mt
+  { start: 6, end: 7, name: "B2", processType: 1, isOffMainThread: false },
+  // This should never collapse.
+  { start: 6, end: 7, name: "C2", processType: 1, isOffMainThread: true },
+
+  { start: 9, end: 12, name: "A2-mt", processType: 2, isOffMainThread: false },
+  // This should collapse only under A2-mt
+  { start: 10, end: 11, name: "D1", processType: 2, isOffMainThread: false },
+  // This should never collapse.
+  { start: 10, end: 11, name: "E1", processType: 2, isOffMainThread: true },
+
+  { start: 13, end: 16, name: "A2-otmt", processType: 2, isOffMainThread: true },
+  // This should collapse only under A2-mt
+  { start: 14, end: 15, name: "D2", processType: 2, isOffMainThread: false },
+  // This should never collapse.
+  { start: 14, end: 15, name: "E2", processType: 2, isOffMainThread: true },
+
+  // This should not collapse, because there's no parent in this process.
+  { start: 14, end: 15, name: "F", processType: 3, isOffMainThread: false },
+
+  // This should never collapse.
+  { start: 14, end: 15, name: "G", processType: 3, isOffMainThread: true },
+];
+
+const gExpectedOutput = {
+  name: "(root)",
+  submarkers: [{
+    start: 1,
+    end: 4,
+    name: "A1-mt",
+    processType: 1,
+    isOffMainThread: false,
+    submarkers: [{
+      start: 2,
+      end: 3,
+      name: "B1",
+      processType: 1,
+      isOffMainThread: false
+    }]
+  }, {
+    start: 2,
+    end: 3,
+    name: "C1",
+    processType: 1,
+    isOffMainThread: true
+  }, {
+    start: 5,
+    end: 8,
+    name: "A1-otmt",
+    processType: 1,
+    isOffMainThread: true,
+    submarkers: [{
+      start: 6,
+      end: 7,
+      name: "B2",
+      processType: 1,
+      isOffMainThread: false
+    }]
+  }, {
+    start: 6,
+    end: 7,
+    name: "C2",
+    processType: 1,
+    isOffMainThread: true
+  }, {
+    start: 9,
+    end: 12,
+    name: "A2-mt",
+    processType: 2,
+    isOffMainThread: false,
+    submarkers: [{
+      start: 10,
+      end: 11,
+      name: "D1",
+      processType: 2,
+      isOffMainThread: false
+    }]
+  }, {
+    start: 10,
+    end: 11,
+    name: "E1",
+    processType: 2,
+    isOffMainThread: true
+  }, {
+    start: 13,
+    end: 16,
+    name: "A2-otmt",
+    processType: 2,
+    isOffMainThread: true,
+    submarkers: [{
+      start: 14,
+      end: 15,
+      name: "D2",
+      processType: 2,
+      isOffMainThread: false
+    }]
+  }, {
+    start: 14,
+    end: 15,
+    name: "E2",
+    processType: 2,
+    isOffMainThread: true
+  }, {
+    start: 14,
+    end: 15,
+    name: "F",
+    processType: 3,
+    isOffMainThread: false,
+    submarkers: []
+  }, {
+    start: 14,
+    end: 15,
+    name: "G",
+    processType: 3,
+    isOffMainThread: true,
+    submarkers: []
+  }]
+};
--- a/devtools/client/performance/test/unit/xpcshell.ini
+++ b/devtools/client/performance/test/unit/xpcshell.ini
@@ -28,8 +28,9 @@ skip-if = toolkit == 'android' || toolki
 [test_tree-model-12.js]
 [test_tree-model-13.js]
 [test_tree-model-allocations-01.js]
 [test_tree-model-allocations-02.js]
 [test_waterfall-utils-collapse-01.js]
 [test_waterfall-utils-collapse-02.js]
 [test_waterfall-utils-collapse-03.js]
 [test_waterfall-utils-collapse-04.js]
+[test_waterfall-utils-collapse-05.js]
--- a/devtools/client/styleinspector/test/head.js
+++ b/devtools/client/styleinspector/test/head.js
@@ -1153,10 +1153,12 @@ function waitForStyleEditor(toolbox, hre
  * @param {InspectorPanel} inspector
  *        The instance of InspectorPanel currently loaded in the toolbox
  * @return a promise that resolves after page reload and inspector
  * initialization
  */
 function reloadPage(inspector) {
   let onNewRoot = inspector.once("new-root");
   content.location.reload();
-  return onNewRoot.then(inspector.markup._waitForChildren);
+  return onNewRoot.then(() => {
+    inspector.markup._waitForChildren();
+  });
 }
--- a/devtools/client/themes/performance.css
+++ b/devtools/client/themes/performance.css
@@ -501,16 +501,27 @@
 
 .waterfall-marker > .theme-twisty {
   /* Don't affect layout. */
   width: 14px;
   -moz-margin-end: -14px;
 }
 
 /**
+ * OTMT markers
+ */
+
+.waterfall-tree-item[otmt=true] .waterfall-marker-bullet,
+.waterfall-tree-item[otmt=true] .waterfall-marker-bar {
+  background-color: transparent;
+  border-width: 1px;
+  border-style: solid;
+}
+
+/**
  * Marker details view
  */
 
 #waterfall-details {
   -moz-padding-start: 8px;
   -moz-padding-end: 8px;
   padding-top: 2vh;
   overflow: auto;
@@ -547,53 +558,63 @@
 
 /**
  * Marker colors
  */
 
 menuitem.marker-color-graphs-full-red:before,
 .marker-color-graphs-full-red {
   background-color: var(--theme-graphs-full-red);
+  border-color: var(--theme-graphs-full-red);
 }
 menuitem.marker-color-graphs-full-blue:before,
 .marker-color-graphs-full-blue {
   background-color: var(--theme-graphs-full-blue);
+  border-color: var(--theme-graphs-full-blue);
 }
 
 menuitem.marker-color-graphs-green:before,
 .marker-color-graphs-green {
   background-color: var(--theme-graphs-green);
+  border-color: var(--theme-graphs-green);
 }
 menuitem.marker-color-graphs-blue:before,
 .marker-color-graphs-blue {
   background-color: var(--theme-graphs-blue);
+  border-color: var(--theme-graphs-blue);
 }
 menuitem.marker-color-graphs-bluegrey:before,
 .marker-color-graphs-bluegrey {
   background-color: var(--theme-graphs-bluegrey);
+  border-color: var(--theme-graphs-bluegrey);
 }
 menuitem.marker-color-graphs-purple:before,
 .marker-color-graphs-purple {
   background-color: var(--theme-graphs-purple);
+  border-color: var(--theme-graphs-purple);
 }
 menuitem.marker-color-graphs-yellow:before,
 .marker-color-graphs-yellow {
   background-color: var(--theme-graphs-yellow);
+  border-color: var(--theme-graphs-yellow);
 }
 menuitem.marker-color-graphs-orange:before,
 .marker-color-graphs-orange {
   background-color: var(--theme-graphs-orange);
+  border-color: var(--theme-graphs-orange);
 }
 menuitem.marker-color-graphs-red:before,
 .marker-color-graphs-red {
   background-color: var(--theme-graphs-red);
+  border-color: var(--theme-graphs-red);
 }
 menuitem.marker-color-graphs-grey:before,
 .marker-color-graphs-grey{
   background-color: var(--theme-graphs-grey);
+  border-color: var(--theme-graphs-grey);
 }
 
 /**
  * JIT View
  */
 
 #jit-optimizations-view {
   width: 350px;
--- a/devtools/server/actors/script.js
+++ b/devtools/server/actors/script.js
@@ -809,16 +809,27 @@ ThreadActor.prototype = {
         return pauseAndRespond(this);
       };
     }
 
     // Otherwise take what a "step" means into consideration.
     return function () {
       // onStep is called with 'this' set to the current frame.
 
+      // Only allow stepping stops at entry points for the line, when
+      // the stepping occurs in a single frame.  The "same frame"
+      // check makes it so a sequence of steps can step out of a frame
+      // and into subsequent calls in the outer frame.  E.g., if there
+      // is a call "a(b())" and the user steps into b, then this
+      // condition makes it possible to step out of b and into a.
+      if (this === startFrame &&
+          !this.script.getOffsetLocation(this.offset).isEntryPoint) {
+        return undefined;
+      }
+
       const generatedLocation = thread.sources.getFrameLocation(this);
       const newLocation = thread.unsafeSynchronize(thread.sources.getOriginalLocation(
         generatedLocation));
 
       // Cases when we should pause because we have executed enough to consider
       // a "step" to have occured:
       //
       // 1.1. We change frames.
--- a/devtools/server/actors/webbrowser.js
+++ b/devtools/server/actors/webbrowser.js
@@ -1970,16 +1970,20 @@ RemoteBrowserTabActor.prototype = {
   },
 
   update: function() {
     // If the child happens to be crashed/close/detach, it won't have _form set,
     // so only request form update if some code is still listening on the other side.
     if (this._form) {
       let deferred = promise.defer();
       let onFormUpdate = msg => {
+        // There may be more than just one childtab.js up and running
+        if (this._form.actor != msg.json.actor) {
+          return;
+        }
         this._mm.removeMessageListener("debug:form", onFormUpdate);
         this._form = msg.json;
         deferred.resolve(this);
       };
       this._mm.addMessageListener("debug:form", onFormUpdate);
       this._mm.sendAsyncMessage("debug:form");
       return deferred.promise;
     } else {
--- a/devtools/shared/Loader.jsm
+++ b/devtools/shared/Loader.jsm
@@ -206,31 +206,30 @@ SrcdirProvider.prototype = {
   _writeManifest: function(srcDir) {
     let clientDir = OS.Path.join(srcDir, "devtools", "client");
     return this._readFile(OS.Path.join(clientDir, "jar.mn")).then((data) => {
       // The file data is contained within inputStream.
       // You can read it into a string with
       let entries = [];
       let lines = data.split(/\n/);
       let preprocessed = /^\s*\*/;
-      let contentEntry =
-        new RegExp("^\\s+content/(\\w+)/(\\S+)\\s+\\((\\S+)\\)");
+      let contentEntry = /^\s+content\/(\S+)\s+\((\S+)\)/;
       for (let line of lines) {
         if (preprocessed.test(line)) {
           dump("Unable to override preprocessed file: " + line + "\n");
           continue;
         }
         let match = contentEntry.exec(line);
         if (match) {
-          let pathComponents = match[3].split("/");
+          let pathComponents = match[2].split("/");
           pathComponents.unshift(clientDir);
           let path = OS.Path.join.apply(OS.Path, pathComponents);
           let uri = this.fileURI(path);
-          let entry = "override chrome://" + match[1] +
-                      "/content/" + match[2] + "\t" + uri;
+          let chromeURI = "chrome://devtools/content/" + match[1];
+          let entry = "override " + chromeURI + "\t" + uri;
           entries.push(entry);
         }
       }
       return this._writeFile(OS.Path.join(clientDir, "chrome.manifest"),
                              entries.join("\n"));
     }).then(() => {
       let clientDirFile = new FileUtils.File(clientDir);
       Components.manager.addBootstrappedManifestLocation(clientDirFile);
--- a/docshell/base/timeline/AbstractTimelineMarker.cpp
+++ b/docshell/base/timeline/AbstractTimelineMarker.cpp
@@ -1,33 +1,40 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=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/. */
 
 #include "AbstractTimelineMarker.h"
+
 #include "mozilla/TimeStamp.h"
+#include "MainThreadUtils.h"
+#include "nsAppRunner.h"
 
 namespace mozilla {
 
 AbstractTimelineMarker::AbstractTimelineMarker(const char* aName,
                                                MarkerTracingType aTracingType)
   : mName(aName)
   , mTracingType(aTracingType)
+  , mProcessType(XRE_GetProcessType())
+  , mIsOffMainThread(!NS_IsMainThread())
 {
   MOZ_COUNT_CTOR(AbstractTimelineMarker);
   SetCurrentTime();
 }
 
 AbstractTimelineMarker::AbstractTimelineMarker(const char* aName,
                                                const TimeStamp& aTime,
                                                MarkerTracingType aTracingType)
   : mName(aName)
   , mTracingType(aTracingType)
+  , mProcessType(XRE_GetProcessType())
+  , mIsOffMainThread(!NS_IsMainThread())
 {
   MOZ_COUNT_CTOR(AbstractTimelineMarker);
   SetCustomTime(aTime);
 }
 
 UniquePtr<AbstractTimelineMarker>
 AbstractTimelineMarker::Clone()
 {
@@ -63,9 +70,21 @@ AbstractTimelineMarker::SetCustomTime(co
 }
 
 void
 AbstractTimelineMarker::SetCustomTime(DOMHighResTimeStamp aTime)
 {
   mTime = aTime;
 }
 
+void
+AbstractTimelineMarker::SetProcessType(GeckoProcessType aProcessType)
+{
+  mProcessType = aProcessType;
+}
+
+void
+AbstractTimelineMarker::SetOffMainThread(bool aIsOffMainThread)
+{
+  mIsOffMainThread = aIsOffMainThread;
+}
+
 } // namespace mozilla
--- a/docshell/base/timeline/AbstractTimelineMarker.h
+++ b/docshell/base/timeline/AbstractTimelineMarker.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 mozilla_AbstractTimelineMarker_h_
 #define mozilla_AbstractTimelineMarker_h_
 
 #include "TimelineMarkerEnums.h" // for MarkerTracingType
 #include "nsDOMNavigationTiming.h" // for DOMHighResTimeStamp
+#include "nsXULAppAPI.h" // for GeckoProcessType
 #include "mozilla/UniquePtr.h"
 
 struct JSContext;
 class JSObject;
 
 namespace mozilla {
 class TimeStamp;
 
@@ -43,22 +44,30 @@ public:
 
   virtual void AddDetails(JSContext* aCx, dom::ProfileTimelineMarker& aMarker) = 0;
   virtual JSObject* GetStack() = 0;
 
   const char* GetName() const { return mName; }
   DOMHighResTimeStamp GetTime() const { return mTime; }
   MarkerTracingType GetTracingType() const { return mTracingType; }
 
+  const uint8_t GetProcessType() const { return mProcessType; };
+  const bool IsOffMainThread() const { return mIsOffMainThread; };
+
 private:
   const char* mName;
   DOMHighResTimeStamp mTime;
   MarkerTracingType mTracingType;
 
+  uint8_t mProcessType; // @see `enum GeckoProcessType`.
+  bool mIsOffMainThread;
+
 protected:
   void SetCurrentTime();
   void SetCustomTime(const TimeStamp& aTime);
   void SetCustomTime(DOMHighResTimeStamp aTime);
+  void SetProcessType(GeckoProcessType aProcessType);
+  void SetOffMainThread(bool aIsOffMainThread);
 };
 
 } // namespace mozilla
 
 #endif /* mozilla_AbstractTimelineMarker_h_ */
new file mode 100644
--- /dev/null
+++ b/docshell/base/timeline/CompositeTimelineMarker.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=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/. */
+
+#ifndef mozilla_CompositeTimelineMarker_h_
+#define mozilla_CompositeTimelineMarker_h_
+
+#include "TimelineMarker.h"
+#include "mozilla/dom/ProfileTimelineMarkerBinding.h"
+
+namespace mozilla {
+
+class CompositeTimelineMarker : public TimelineMarker
+{
+public:
+  explicit CompositeTimelineMarker(const TimeStamp& aTime,
+                                   MarkerTracingType aTracingType)
+    : TimelineMarker("Composite", aTime, aTracingType)
+  {
+    // Even though these markers end up being created on the main thread in the
+    // content or chrome processes, they actually trace down code in the
+    // compositor parent process. All the information for creating these markers
+    // is sent along via IPC to an nsView when a composite finishes.
+    // Mark this as 'off the main thread' to style it differently in frontends.
+    SetOffMainThread(true);
+  }
+};
+
+} // namespace mozilla
+
+#endif // mozilla_CompositeTimelineMarker_h_
--- a/docshell/base/timeline/ConsoleTimelineMarker.h
+++ b/docshell/base/timeline/ConsoleTimelineMarker.h
@@ -35,16 +35,18 @@ public:
     // Console markers must have matching causes as well. It is safe to perform
     // a static_cast here as the previous equality check ensures that this is
     // a console marker instance.
     return mCause == static_cast<const ConsoleTimelineMarker*>(&aOther)->mCause;
   }
 
   virtual void AddDetails(JSContext* aCx, dom::ProfileTimelineMarker& aMarker) override
   {
+    TimelineMarker::AddDetails(aCx, aMarker);
+
     if (GetTracingType() == MarkerTracingType::START) {
       aMarker.mCauseName.Construct(mCause);
     } else {
       aMarker.mEndStack = GetStack();
     }
   }
 
 private:
--- a/docshell/base/timeline/EventTimelineMarker.h
+++ b/docshell/base/timeline/EventTimelineMarker.h
@@ -20,16 +20,18 @@ public:
                                MarkerTracingType aTracingType)
     : TimelineMarker("DOMEvent", aTracingType)
     , mType(aType)
     , mPhase(aPhase)
   {}
 
   virtual void AddDetails(JSContext* aCx, dom::ProfileTimelineMarker& aMarker) override
   {
+    TimelineMarker::AddDetails(aCx, aMarker);
+
     if (GetTracingType() == MarkerTracingType::START) {
       aMarker.mType.Construct(mType);
       aMarker.mEventPhase.Construct(mPhase);
     }
   }
 
 private:
   nsString mType;
--- a/docshell/base/timeline/JavascriptTimelineMarker.h
+++ b/docshell/base/timeline/JavascriptTimelineMarker.h
@@ -26,16 +26,18 @@ public:
     , mCause(NS_ConvertUTF8toUTF16(aReason))
     , mFunctionName(aFunctionName)
     , mFileName(aFileName)
     , mLineNumber(aLineNumber)
   {}
 
   virtual void AddDetails(JSContext* aCx, dom::ProfileTimelineMarker& aMarker) override
   {
+    TimelineMarker::AddDetails(aCx, aMarker);
+
     aMarker.mCauseName.Construct(mCause);
 
     if (!mFunctionName.IsEmpty() || !mFileName.IsEmpty()) {
       dom::RootedDictionary<dom::ProfileTimelineStackFrame> stackFrame(aCx);
       stackFrame.mLine.Construct(mLineNumber);
       stackFrame.mSource.Construct(mFileName);
       stackFrame.mFunctionDisplayName.Construct(mFunctionName);
 
--- a/docshell/base/timeline/RestyleTimelineMarker.h
+++ b/docshell/base/timeline/RestyleTimelineMarker.h
@@ -21,16 +21,18 @@ public:
   {
     if (aRestyleHint) {
       mRestyleHint.AssignWithConversion(RestyleManager::RestyleHintToString(aRestyleHint));
     }
   }
 
   virtual void AddDetails(JSContext* aCx, dom::ProfileTimelineMarker& aMarker) override
   {
+    TimelineMarker::AddDetails(aCx, aMarker);
+
     if (GetTracingType() == MarkerTracingType::START) {
       aMarker.mRestyleHint.Construct(mRestyleHint);
     }
   }
 
 private:
   nsAutoString mRestyleHint;
 };
--- a/docshell/base/timeline/TimelineMarker.cpp
+++ b/docshell/base/timeline/TimelineMarker.cpp
@@ -23,17 +23,20 @@ TimelineMarker::TimelineMarker(const cha
   : AbstractTimelineMarker(aName, aTime, aTracingType)
 {
   CaptureStackIfNecessary(aTracingType, aStackRequest);
 }
 
 void
 TimelineMarker::AddDetails(JSContext* aCx, dom::ProfileTimelineMarker& aMarker)
 {
-  // Nothing to do here for plain markers.
+  if (GetTracingType() == MarkerTracingType::START) {
+    aMarker.mProcessType.Construct(GetProcessType());
+    aMarker.mIsOffMainThread.Construct(IsOffMainThread());
+  }
 }
 
 JSObject*
 TimelineMarker::GetStack()
 {
   if (mStackTrace.initialized()) {
     return mStackTrace;
   }
--- a/docshell/base/timeline/TimestampTimelineMarker.h
+++ b/docshell/base/timeline/TimestampTimelineMarker.h
@@ -17,16 +17,18 @@ class TimestampTimelineMarker : public T
 public:
   explicit TimestampTimelineMarker(const nsAString& aCause)
     : TimelineMarker("TimeStamp", MarkerTracingType::TIMESTAMP)
     , mCause(aCause)
   {}
 
   virtual void AddDetails(JSContext* aCx, dom::ProfileTimelineMarker& aMarker) override
   {
+    TimelineMarker::AddDetails(aCx, aMarker);
+
     if (!mCause.IsEmpty()) {
       aMarker.mCauseName.Construct(mCause);
     }
   }
 
 private:
   nsString mCause;
 };
--- a/docshell/base/timeline/WorkerTimelineMarker.h
+++ b/docshell/base/timeline/WorkerTimelineMarker.h
@@ -25,16 +25,18 @@ public:
   {
     WorkerTimelineMarker* clone = new WorkerTimelineMarker(mOperationType, GetTracingType());
     clone->SetCustomTime(GetTime());
     return UniquePtr<AbstractTimelineMarker>(clone);
   }
 
   virtual void AddDetails(JSContext* aCx, dom::ProfileTimelineMarker& aMarker) override
   {
+    TimelineMarker::AddDetails(aCx, aMarker);
+
     if (GetTracingType() == MarkerTracingType::START) {
       aMarker.mWorkerOperation.Construct(mOperationType);
     }
   }
 
 private:
   ProfileTimelineWorkerOperationType mOperationType;
 };
--- a/docshell/base/timeline/moz.build
+++ b/docshell/base/timeline/moz.build
@@ -3,16 +3,17 @@
 # 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/.
 
 EXPORTS.mozilla += [
     'AbstractTimelineMarker.h',
     'AutoGlobalTimelineMarker.h',
     'AutoTimelineMarker.h',
+    'CompositeTimelineMarker.h',
     'ConsoleTimelineMarker.h',
     'EventTimelineMarker.h',
     'JavascriptTimelineMarker.h',
     'LayerTimelineMarker.h',
     'MarkersStorage.h',
     'ObservedDocShell.h',
     'RestyleTimelineMarker.h',
     'TimelineConsumers.h',
--- a/docshell/test/browser/browser_timelineMarkers-frame-02.js
+++ b/docshell/test/browser/browser_timelineMarkers-frame-02.js
@@ -6,24 +6,31 @@
 // Test that the docShell profile timeline API returns the right markers when
 // restyles, reflows and paints occur
 
 function rectangleContains(rect, x, y, width, height) {
   return rect.x <= x && rect.y <= y && rect.width >= width &&
     rect.height >= height;
 }
 
+function sanitizeMarkers(list) {
+  // Worker markers are currently gathered from all docshells, which may
+  // interfere with this test.
+  return list.filter(e => e.name != "Worker")
+}
+
 var TESTS = [{
   desc: "Changing the width of the test element",
   searchFor: "Paint",
   setup: function(docShell) {
     let div = content.document.querySelector("div");
     div.setAttribute("class", "resize-change-color");
   },
   check: function(markers) {
+    markers = sanitizeMarkers(markers);
     ok(markers.length > 0, "markers were returned");
     console.log(markers);
     info(JSON.stringify(markers.filter(m => m.name == "Paint")));
     ok(markers.some(m => m.name == "Reflow"), "markers includes Reflow");
     ok(markers.some(m => m.name == "Paint"), "markers includes Paint");
     for (let marker of markers.filter(m => m.name == "Paint")) {
       // This change should generate at least one rectangle.
       ok(marker.rectangles.length >= 1, "marker has one rectangle");
@@ -35,16 +42,17 @@ var TESTS = [{
 }, {
   desc: "Changing the test element's background color",
   searchFor: "Paint",
   setup: function(docShell) {
     let div = content.document.querySelector("div");
     div.setAttribute("class", "change-color");
   },
   check: function(markers) {
+    markers = sanitizeMarkers(markers);
     ok(markers.length > 0, "markers were returned");
     ok(!markers.some(m => m.name == "Reflow"), "markers doesn't include Reflow");
     ok(markers.some(m => m.name == "Paint"), "markers includes Paint");
     for (let marker of markers.filter(m => m.name == "Paint")) {
       // This change should generate at least one rectangle.
       ok(marker.rectangles.length >= 1, "marker has one rectangle");
       // One of the rectangles should contain the div.
       ok(marker.rectangles.some(r => rectangleContains(r, 0, 0, 50, 50)));
@@ -54,16 +62,17 @@ var TESTS = [{
 }, {
   desc: "Changing the test element's classname",
   searchFor: "Paint",
   setup: function(docShell) {
     let div = content.document.querySelector("div");
     div.setAttribute("class", "change-color add-class");
   },
   check: function(markers) {
+    markers = sanitizeMarkers(markers);
     ok(markers.length > 0, "markers were returned");
     ok(!markers.some(m => m.name == "Reflow"), "markers doesn't include Reflow");
     ok(!markers.some(m => m.name == "Paint"), "markers doesn't include Paint");
     ok(markers.some(m => m.name == "Styles"), "markers includes Restyle");
   }
 }, {
   desc: "sync console.time/timeEnd",
   searchFor: "ConsoleTime",
@@ -79,16 +88,17 @@ var TESTS = [{
       content.console.time("BAR");
       content.setTimeout(() => {
         content.console.timeEnd("FOO");
         content.console.timeEnd("BAR");
       }, 100);
     }, 100);
   },
   check: function(markers) {
+    markers = sanitizeMarkers(markers);
     is(markers.length, 2, "Got 2 markers");
     is(markers[0].name, "ConsoleTime", "Got first ConsoleTime marker");
     is(markers[0].causeName, "FOO", "Got ConsoleTime FOO detail");
     is(markers[1].name, "ConsoleTime", "Got second ConsoleTime marker");
     is(markers[1].causeName, "BAR", "Got ConsoleTime BAR detail");
   }
 }, {
   desc: "Timestamps created by console.timeStamp()",
@@ -100,17 +110,17 @@ var TESTS = [{
     is(markers[0].name, "TimeStamp", "Got Timestamp marker");
     is(markers[0].causeName, "rock", "Got Timestamp label value");
     content.console.timeStamp("paper");
     content.console.timeStamp("scissors");
     content.console.timeStamp();
     content.console.timeStamp(undefined);
   },
   check: function (markers) {
-    markers = markers.filter(e => e.name != "Worker");
+    markers = sanitizeMarkers(markers);
     is(markers.length, 4, "Got 4 markers");
     is(markers[0].name, "TimeStamp", "Got Timestamp marker");
     is(markers[0].causeName, "paper", "Got Timestamp label value");
     is(markers[1].name, "TimeStamp", "Got Timestamp marker");
     is(markers[1].causeName, "scissors", "Got Timestamp label value");
     is(markers[2].name, "TimeStamp", "Got empty Timestamp marker when no argument given");
     is(markers[2].causeName, void 0, "Got empty Timestamp label value");
     is(markers[3].name, "TimeStamp", "Got empty Timestamp marker when argument is undefined");
--- a/dom/webidl/ProfileTimelineMarker.webidl
+++ b/dom/webidl/ProfileTimelineMarker.webidl
@@ -33,16 +33,19 @@ enum ProfileTimelineWorkerOperationType 
 };
 
 dictionary ProfileTimelineMarker {
   DOMString name = "";
   DOMHighResTimeStamp start = 0;
   DOMHighResTimeStamp end = 0;
   object? stack = null;
 
+  unsigned short processType;
+  boolean isOffMainThread;
+
   /* For ConsoleTime, Timestamp and Javascript markers.  */
   DOMString causeName;
 
   /* For ConsoleTime markers.  */
   object? endStack = null;
 
   /* For DOMEvent markers.  */
   DOMString type;
--- a/js/src/doc/Debugger/Debugger.Script.md
+++ b/js/src/doc/Debugger/Debugger.Script.md
@@ -213,16 +213,19 @@ methods of other kinds of objects.
 :   Return an object describing the source code location responsible for the
     bytecode at <i>offset</i> in this script.  The object has the
     following properties:
 
     * lineNumber: the line number for which offset is an entry point
 
     * columnNumber: the column number for which offset is an entry point
 
+    * isEntryPoint: true if the offset is a column entry point, as
+      would be reported by getAllColumnOffsets(); otherwise false.
+
 `getOffsetsCoverage()`:
 :   Return `null` or an array which contains informations about the coverage of
     all opcodes. The elements of the array are objects, each of which describes
     a single opcode, and contains the following properties:
 
     * lineNumber: the line number of the current opcode.
 
     * columnNumber: the column number of the current opcode.
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-11.js
@@ -0,0 +1,36 @@
+// Stepping out of a finally should not appear to
+// step backward to some earlier statement.
+
+var g = newGlobal();
+g.eval(`function f() {
+         debugger;              // +0
+         var x = 0;             // +1
+         try {                  // +2
+           x = 1;               // +3
+           throw 'something';   // +4
+         } catch (e) {          // +5
+           x = 2;               // +6
+         } finally {            // +7
+           x = 3;               // +8
+         }                      // +9
+         x = 4;                 // +10
+       }`);                     // +11
+
+var dbg = Debugger(g);
+
+let foundLines = '';
+
+dbg.onDebuggerStatement = function(frame) {
+  let debugLine = frame.script.getOffsetLocation(frame.offset).lineNumber;
+  frame.onStep = function() {
+    // Only record a stop when the offset is an entry point.
+    let foundLine = this.script.getOffsetLocation(this.offset).lineNumber;
+    if (foundLine != debugLine && this.script.getLineOffsets(foundLine).indexOf(this.offset) >= 0) {
+      foundLines += "," + (foundLine - debugLine);
+    }
+  };
+};
+
+g.f();
+
+assertEq(foundLines, ",1,2,3,4,6,7,8,10");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-12.js
@@ -0,0 +1,129 @@
+// Because our script source notes record only those bytecode offsets
+// at which source positions change, the default behavior in the
+// absence of a source note is to attribute a bytecode instruction to
+// the same source location as the preceding instruction. When control
+// flows from the preceding bytecode to the one we're emitting, that's
+// usually plausible. But successors in the bytecode stream are not
+// necessarily successors in the control flow graph. If the preceding
+// bytecode was a back edge of a loop, or the jump at the end of a
+// 'then' clause, its source position can be completely unrelated to
+// that of its successor.
+
+// We try to avoid showing such nonsense offsets to the user by
+// requiring breakpoints and single-stepping to stop only at a line's
+// entry points, as reported by Debugger.Script.prototype.getLineOffsets;
+// and then ensuring that those entry points are all offsets mentioned
+// explicitly in the source notes, and hence deliberately attributed
+// to the given bytecode.
+
+// This bit of JavaScript compiles to bytecode ending in a branch
+// instruction whose source position is the body of an unreachable
+// loop. The first instruction of the bytecode we emit following it
+// will inherit this nonsense position, if we have not explicitly
+// emitted a source note for said instruction.
+
+// This test steps across such code and verifies that control never
+// appears to enter the unreachable loop.
+
+var bitOfCode = `debugger;                    // +0
+                 if(false) {                  // +1
+                   for(var b=0; b<0; b++) {   // +2
+                      c = 2                   // +3
+                    }                         // +4
+                 }`;                          // +5
+
+var g = newGlobal();
+var dbg = Debugger(g);
+
+g.eval("function nothing() { }\n");
+
+var log = '';
+dbg.onDebuggerStatement = function(frame) {
+  let debugLine = frame.script.getOffsetLocation(frame.offset).lineNumber;
+  frame.onStep = function() {
+    let foundLine = this.script.getOffsetLocation(this.offset).lineNumber;
+    if (this.script.getLineOffsets(foundLine).indexOf(this.offset) >= 0) {
+      log += (foundLine - debugLine).toString(16);
+    }
+  };
+};
+
+function testOne(name, body, expected) {
+  print(name);
+  log = '';
+  g.eval(`function ${name} () { ${body} }`);
+  g.eval(`${name}();`);
+  assertEq(log, expected);
+}
+
+
+
+// Test the instructions at the end of a "try".
+testOne("testTryFinally",
+        `try {
+           ${bitOfCode}
+         } finally {            // +6
+         }                      // +7
+         nothing();             // +8
+        `, "168");
+
+// The same but without a finally clause.
+testOne("testTryCatch",
+        `try {
+           ${bitOfCode}
+         } catch (e) {          // +6
+         }                      // +7
+         nothing();             // +8
+        `, "18");
+
+// Test the instructions at the end of a "catch".
+testOne("testCatchFinally",
+        `try {
+           throw new TypeError();
+         } catch (e) {
+           ${bitOfCode}
+         } finally {            // +6
+         }                      // +7
+         nothing();             // +8
+        `, "168");
+
+// The same but without a finally clause.  This relies on a
+// SpiderMonkey extension, because otherwise there's no way to see
+// extra instructions at the end of a catch.
+testOne("testCatch",
+        `try {
+           throw new TypeError();
+         } catch (e if e instanceof TypeError) {
+           ${bitOfCode}
+         } catch (e) {          // +6
+         }                      // +7
+         nothing();             // +8
+        `, "18");
+
+// Test the instruction at the end of a "finally" clause.
+testOne("testFinally",
+        `try {
+         } finally {
+           ${bitOfCode}
+         }                      // +6
+         nothing();             // +7
+        `, "17");
+
+// Test the instruction at the end of a "then" clause.
+testOne("testThen",
+        `if (1 === 1) {
+           ${bitOfCode}
+         } else {               // +6
+         }                      // +7
+         nothing();             // +8
+        `, "18");
+
+// Test the instructions leaving a switch block.
+testOne("testSwitch",
+        `var x = 5;
+         switch (x) {
+           case 5:
+             ${bitOfCode}
+         }                      // +6
+         nothing();             // +7
+        `, "17");
--- a/js/src/jit-test/tests/debug/Script-getAllColumnOffsets-06.js
+++ b/js/src/jit-test/tests/debug/Script-getAllColumnOffsets-06.js
@@ -9,19 +9,20 @@ Debugger(global).onDebuggerStatement = f
                 assertEq(offset.lineNumber, 1);
                 global.log += offset.columnNumber + " ";
             }
         });
     });
 };
 
 global.log = "";
+global.eval("function ppppp() { return 1; }");
 //                     1         2         3         4
 //           0123456789012345678901234567890123456789012345678
-global.eval("function f(){ 1 && print(print()) && new Error() } debugger;");
+global.eval("function f(){ 1 && ppppp(ppppp()) && new Error() } debugger;");
 global.f();
 
 // 14 - Enter the function body
 // 25 - Inner print()
 // 19 - Outer print()
 // 37 - new Error()
 // 48 - Exit the function body
 assertEq(global.log, "14 25 19 37 48 ");
--- a/js/src/jit-test/tests/debug/Script-getOffsetLocation.js
+++ b/js/src/jit-test/tests/debug/Script-getOffsetLocation.js
@@ -1,24 +1,34 @@
 // getOffsetLocation agrees with getAllColumnOffsets
 
 var global = newGlobal();
 Debugger(global).onDebuggerStatement = function (frame) {
-    var script = frame.eval("f").return.script;
+    var script = frame.script;
+    var byOffset = [];
     script.getAllColumnOffsets().forEach(function (entry) {
         var {lineNumber, columnNumber, offset} = entry;
+        byOffset[offset] = {lineNumber, columnNumber};
+    });
+
+    frame.onStep = function() {
+        var offset = frame.offset;
         var location = script.getOffsetLocation(offset);
-        assertEq(location.lineNumber, lineNumber);
-        assertEq(location.columnNumber, columnNumber);
-    });
+        if (location.isEntryPoint) {
+            assertEq(location.lineNumber, byOffset[offset].lineNumber);
+            assertEq(location.columnNumber, byOffset[offset].columnNumber);
+        } else {
+            assertEq(byOffset[offset], undefined);
+        }
+    };
 };
 
 function test(body) {
   print("Test: " + body);
-  global.eval(`function f(n) { ${body} } debugger;`);
+  global.eval(`function f(n) { debugger; ${body} }`);
   global.f(3);
 }
 
 test("for (var i = 0; i < n; ++i) ;");
 test("var w0,x1=3,y2=4,z3=9");
 test("print(n),print(n),print(n)");
 test("var o={a:1,b:2,c:3}");
 test("var a=[1,2,n]");
--- a/js/src/vm/CommonPropertyNames.h
+++ b/js/src/vm/CommonPropertyNames.h
@@ -119,16 +119,17 @@
     macro(isNaN, isNaN, "isNaN") \
     macro(isPrototypeOf, isPrototypeOf, "isPrototypeOf") \
     macro(iterate, iterate, "iterate") \
     macro(Infinity, Infinity, "Infinity") \
     macro(InterpretGeneratorResume, InterpretGeneratorResume, "InterpretGeneratorResume") \
     macro(int8, int8, "int8") \
     macro(int16, int16, "int16") \
     macro(int32, int32, "int32") \
+    macro(isEntryPoint, isEntryPoint, "isEntryPoint") \
     macro(isExtensible, isExtensible, "isExtensible") \
     macro(iteratorIntrinsic, iteratorIntrinsic, "__iterator__") \
     macro(join, join, "join") \
     macro(keys, keys, "keys") \
     macro(label, label, "label") \
     macro(lastIndex, lastIndex, "lastIndex") \
     macro(LegacyGeneratorCloseInternal, LegacyGeneratorCloseInternal, "LegacyGeneratorCloseInternal") \
     macro(length, length, "length") \
--- a/js/src/vm/Debugger.cpp
+++ b/js/src/vm/Debugger.cpp
@@ -4851,63 +4851,81 @@ class BytecodeRangeWithPosition : privat
   public:
     using BytecodeRange::empty;
     using BytecodeRange::frontPC;
     using BytecodeRange::frontOpcode;
     using BytecodeRange::frontOffset;
 
     BytecodeRangeWithPosition(JSContext* cx, JSScript* script)
       : BytecodeRange(cx, script), lineno(script->lineno()), column(0),
-        sn(script->notes()), snpc(script->code())
+        sn(script->notes()), snpc(script->code()), isEntryPoint(false)
     {
         if (!SN_IS_TERMINATOR(sn))
             snpc += SN_DELTA(sn);
         updatePosition();
         while (frontPC() != script->main())
             popFront();
+        isEntryPoint = true;
     }
 
     void popFront() {
         BytecodeRange::popFront();
-        if (!empty())
+        if (empty())
+            isEntryPoint = false;
+        else
             updatePosition();
     }
 
     size_t frontLineNumber() const { return lineno; }
     size_t frontColumnNumber() const { return column; }
 
+    // Entry points are restricted to bytecode offsets that have an
+    // explicit mention in the line table.  This restriction avoids a
+    // number of failing cases caused by some instructions not having
+    // sensible (to the user) line numbers, and it is one way to
+    // implement the idea that the bytecode emitter should tell the
+    // debugger exactly which offsets represent "interesting" (to the
+    // user) places to stop.
+    bool frontIsEntryPoint() const { return isEntryPoint; }
+
   private:
     void updatePosition() {
         /*
          * Determine the current line number by reading all source notes up to
          * and including the current offset.
          */
+        jsbytecode *lastLinePC = nullptr;
         while (!SN_IS_TERMINATOR(sn) && snpc <= frontPC()) {
             SrcNoteType type = (SrcNoteType) SN_TYPE(sn);
             if (type == SRC_COLSPAN) {
                 ptrdiff_t colspan = SN_OFFSET_TO_COLSPAN(GetSrcNoteOffset(sn, 0));
                 MOZ_ASSERT(ptrdiff_t(column) + colspan >= 0);
                 column += colspan;
+                lastLinePC = snpc;
             } else if (type == SRC_SETLINE) {
                 lineno = size_t(GetSrcNoteOffset(sn, 0));
                 column = 0;
+                lastLinePC = snpc;
             } else if (type == SRC_NEWLINE) {
                 lineno++;
                 column = 0;
+                lastLinePC = snpc;
             }
 
             sn = SN_NEXT(sn);
             snpc += SN_DELTA(sn);
         }
+        isEntryPoint = lastLinePC == frontPC();
     }
 
     size_t lineno;
     size_t column;
     jssrcnote* sn;
     jsbytecode* snpc;
+    bool isEntryPoint;
 };
 
 /*
  * FlowGraphSummary::populate(cx, script) computes a summary of script's
  * control flow graph used by DebuggerScript_{getAllOffsets,getLineOffsets}.
  *
  * An instruction on a given line is an entry point for that line if it can be
  * reached from (an instruction on) a different line. We distinguish between the
@@ -5078,16 +5096,20 @@ DebuggerScript_getOffsetLocation(JSConte
 {
     THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "getOffsetLocation", args, obj, script);
     if (!args.requireAtLeast(cx, "Debugger.Script.getOffsetLocation", 1))
         return false;
     size_t offset;
     if (!ScriptOffset(cx, script, args[0], &offset))
         return false;
 
+    FlowGraphSummary flowData(cx);
+    if (!flowData.populate(cx, script))
+        return false;
+
     RootedPlainObject result(cx, NewBuiltinClassInstance<PlainObject>(cx));
     if (!result)
         return false;
 
     BytecodeRangeWithPosition r(cx, script);
     while (!r.empty() && r.frontOffset() < offset)
         r.popFront();
 
@@ -5095,16 +5117,25 @@ DebuggerScript_getOffsetLocation(JSConte
     RootedValue value(cx, NumberValue(r.frontLineNumber()));
     if (!DefineProperty(cx, result, id, value))
         return false;
 
     value = NumberValue(r.frontColumnNumber());
     if (!DefineProperty(cx, result, cx->names().columnNumber, value))
         return false;
 
+    // The same entry point test that is used by getAllColumnOffsets.
+    bool isEntryPoint = (r.frontIsEntryPoint() &&
+                         !flowData[offset].hasNoEdges() &&
+                         (flowData[offset].lineno() != r.frontLineNumber() ||
+                          flowData[offset].column() != r.frontColumnNumber()));
+    value.setBoolean(isEntryPoint);
+    if (!DefineProperty(cx, result, cx->names().isEntryPoint, value))
+        return false;
+
     args.rval().setObject(*result);
     return true;
 }
 
 static bool
 DebuggerScript_getAllOffsets(JSContext* cx, unsigned argc, Value* vp)
 {
     THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "getAllOffsets", args, obj, script);
@@ -5117,16 +5148,19 @@ DebuggerScript_getAllOffsets(JSContext* 
     if (!flowData.populate(cx, script))
         return false;
 
     /* Second pass: build the result array. */
     RootedObject result(cx, NewDenseEmptyArray(cx));
     if (!result)
         return false;
     for (BytecodeRangeWithPosition r(cx, script); !r.empty(); r.popFront()) {
+        if (!r.frontIsEntryPoint())
+            continue;
+
         size_t offset = r.frontOffset();
         size_t lineno = r.frontLineNumber();
 
         /* Make a note, if the current instruction is an entry point for the current line. */
         if (!flowData[offset].hasNoEdges() && flowData[offset].lineno() != lineno) {
             /* Get the offsets array for this line. */
             RootedObject offsets(cx);
             RootedValue offsetsv(cx);
@@ -5190,17 +5224,18 @@ DebuggerScript_getAllColumnOffsets(JSCon
     if (!result)
         return false;
     for (BytecodeRangeWithPosition r(cx, script); !r.empty(); r.popFront()) {
         size_t lineno = r.frontLineNumber();
         size_t column = r.frontColumnNumber();
         size_t offset = r.frontOffset();
 
         /* Make a note, if the current instruction is an entry point for the current position. */
-        if (!flowData[offset].hasNoEdges() &&
+        if (r.frontIsEntryPoint() &&
+            !flowData[offset].hasNoEdges() &&
             (flowData[offset].lineno() != lineno ||
              flowData[offset].column() != column)) {
             RootedPlainObject entry(cx, NewBuiltinClassInstance<PlainObject>(cx));
             if (!entry)
                 return false;
 
             RootedId id(cx, NameToId(cx->names().lineNumber));
             RootedValue value(cx, NumberValue(lineno));
@@ -5254,16 +5289,19 @@ DebuggerScript_getLineOffsets(JSContext*
     if (!flowData.populate(cx, script))
         return false;
 
     /* Second pass: build the result array. */
     RootedObject result(cx, NewDenseEmptyArray(cx));
     if (!result)
         return false;
     for (BytecodeRangeWithPosition r(cx, script); !r.empty(); r.popFront()) {
+        if (!r.frontIsEntryPoint())
+            continue;
+
         size_t offset = r.frontOffset();
 
         /* If the op at offset is an entry point, append offset to result. */
         if (r.frontLineNumber() == lineno &&
             !flowData[offset].hasNoEdges() &&
             flowData[offset].lineno() != lineno)
         {
             if (!NewbornArrayPush(cx, result, NumberValue(offset)))
new file mode 100644
--- /dev/null
+++ b/testing/eslint-plugin-mozilla/docs/balanced-listeners.rst
@@ -0,0 +1,11 @@
+.. _balanced-listeners:
+
+==================
+balanced-listeners
+==================
+
+Rule Details
+------------
+
+Checks that for every occurences of 'addEventListener' or 'on' there is an
+occurence of 'removeEventListener' or 'off' with the same event name.
--- a/testing/eslint-plugin-mozilla/docs/index.rst
+++ b/testing/eslint-plugin-mozilla/docs/index.rst
@@ -1,23 +1,28 @@
 .. _index:
 
 =====================
 Mozilla ESLint Plugin
 =====================
 
+``balanced-listeners`` checks that every addEventListener has a
+removeEventListener (and does the same for on/off).
+
 ``components-imports`` adds the filename of imported files e.g.
 ``Cu.import("some/path/Blah.jsm")`` adds Blah to the global scope.
 
 ``import-headjs-globals`` imports globals from head.js and from any files that
 should be imported by head.js (as far as we can correctly resolve the path).
 
 ``mark-test-function-used`` simply marks test (the test method) as used. This
 avoids ESLint telling us that the function is never called.
 
+``no-aArgs`` prevents using the hungarian notation in function arguments.
+
 ``var-only-at-top-level`` Marks all var declarations that are not at the top
 level invalid.
 
 +-------+-----------------------+
 | Possible values for all rules |
 +-------+-----------------------+
 | Value | Meaning               |
 +-------+-----------------------+
@@ -26,21 +31,24 @@ level invalid.
 | 1     | Warning               |
 +-------+-----------------------+
 | 2     | Error                 |
 +-------+-----------------------+
 
 Example configuration::
 
    "rules": {
+     "mozilla/balanced-listeners": 2,
      "mozilla/components-imports": 1,
      "mozilla/import-headjs-globals": 1,
      "mozilla/mark-test-function-used": 1,
      "mozilla/var-only-at-top-level": 1,
    }
 
 .. toctree::
    :maxdepth: 1
 
+   balanced-listeners
    components-imports
    import-headjs-globals
    mark-test-function-used
+   no-aArgs
    var-only-at-top-level
new file mode 100644
--- /dev/null
+++ b/testing/eslint-plugin-mozilla/docs/no-aArgs.rst
@@ -0,0 +1,12 @@
+.. _no-aArgs:
+
+========
+no-aArgs
+========
+
+Rule Details
+------------
+
+Checks that function argument names don't start with lowercase 'a' followed by a
+capital letter. This is to prevent the use of Hungarian notation whereby the
+first letter is a prefix that indicates the type or intended use of a variable.
--- a/testing/eslint-plugin-mozilla/lib/index.js
+++ b/testing/eslint-plugin-mozilla/lib/index.js
@@ -8,20 +8,24 @@
 "use strict";
 
 //------------------------------------------------------------------------------
 // Plugin Definition
 //------------------------------------------------------------------------------
 
 module.exports = {
   rules: {
+    "balanced-listeners": require("../lib/rules/balanced-listeners"),
     "components-imports": require("../lib/rules/components-imports"),
     "import-headjs-globals": require("../lib/rules/import-headjs-globals"),
     "mark-test-function-used": require("../lib/rules/mark-test-function-used"),
+    "no-aArgs": require("../lib/rules/no-aArgs"),
     "var-only-at-top-level": require("../lib/rules/var-only-at-top-level")
   },
   rulesConfig: {
+    "balanced-listeners": 0,
     "components-imports": 0,
     "import-headjs-globals": 0,
     "mark-test-function-used": 0,
+    "no-aArgs": 0,
     "var-only-at-top-level": 0
   }
 };
new file mode 100644
--- /dev/null
+++ b/testing/eslint-plugin-mozilla/lib/rules/balanced-listeners.js
@@ -0,0 +1,107 @@
+/**
+ * @fileoverview Check that there's a removeEventListener for each
+ * addEventListener and an off for each on.
+ * Note that for now, this rule is rather simple in that it only checks that
+ * for each event name there is both an add and remove listener. It doesn't
+ * check that these are called on the right objects or with the same callback.
+ *
+ * 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";
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+module.exports = function(context) {
+  //--------------------------------------------------------------------------
+  // Helpers
+  //--------------------------------------------------------------------------
+
+  var DICTIONARY = {
+    "addEventListener": "removeEventListener",
+    "on": "off"
+  };
+  // Invert this dictionary to make it easy later.
+  var INVERTED_DICTIONARY = {};
+  for (var i in DICTIONARY) {
+    INVERTED_DICTIONARY[DICTIONARY[i]] = i;
+  }
+
+  // Collect the add/remove listeners in these 2 arrays.
+  var addedListeners = [];
+  var removedListeners = [];
+
+  function addAddedListener(node) {
+    addedListeners.push({
+      functionName: node.callee.property.name,
+      type: node.arguments[0].value,
+      node: node.callee.property,
+      useCapture: node.arguments[2] ? node.arguments[2].value : null
+    });
+  }
+
+  function addRemovedListener(node) {
+    removedListeners.push({
+      functionName: node.callee.property.name,
+      type: node.arguments[0].value,
+      useCapture: node.arguments[2] ? node.arguments[2].value : null
+    });
+  }
+
+  function getUnbalancedListeners() {
+    var unbalanced = [];
+
+    for (var i = 0; i < addedListeners.length; i ++) {
+      if (!hasRemovedListener(addedListeners[i])) {
+        unbalanced.push(addedListeners[i]);
+      }
+    }
+    addedListeners = removedListeners = [];
+
+    return unbalanced;
+  }
+
+  function hasRemovedListener(addedListener) {
+    for (var i = 0; i < removedListeners.length; i ++) {
+      var listener = removedListeners[i];
+      if (DICTIONARY[addedListener.functionName] === listener.functionName &&
+          addedListener.type === listener.type &&
+          addedListener.useCapture === listener.useCapture) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  //--------------------------------------------------------------------------
+  // Public
+  //--------------------------------------------------------------------------
+
+  return {
+    CallExpression: function(node) {
+      if (node.callee.type === "MemberExpression") {
+        var listenerMethodName = node.callee.property.name;
+
+        if (DICTIONARY.hasOwnProperty(listenerMethodName)) {
+          addAddedListener(node);
+        } else if (INVERTED_DICTIONARY.hasOwnProperty(listenerMethodName)) {
+          addRemovedListener(node);
+        }
+      }
+    },
+
+    "Program:exit": function() {
+      getUnbalancedListeners().forEach(function(listener) {
+        context.report(listener.node,
+          "No corresponding '{{functionName}}({{type}})' was found.", {
+          functionName: DICTIONARY[listener.functionName],
+          type: listener.type
+        });
+      });
+    }
+  };
+};
new file mode 100644
--- /dev/null
+++ b/testing/eslint-plugin-mozilla/lib/rules/no-aArgs.js
@@ -0,0 +1,50 @@
+/**
+ * @fileoverview warns against using hungarian notation in function arguments
+ * (i.e. aArg).
+ *
+ * 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";
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+module.exports = function(context) {
+  //--------------------------------------------------------------------------
+  // Helpers
+  //--------------------------------------------------------------------------
+
+  function isPrefixed(name) {
+    return name.length >= 2 && /^a[A-Z]/.test(name);
+  }
+
+  function deHungarianize(name) {
+    return name.substring(1, 2).toLowerCase() +
+           name.substring(2, name.length);
+  }
+
+  function checkFunction(node) {
+    for (var i = 0; i < node.params.length; i ++) {
+      var param = node.params[i];
+      if (param.name && isPrefixed(param.name)) {
+        context.report(param, "Parameter '{{name}}' uses Hungarian Notation, consider using '{{suggestion}}' instead.", {
+          name: param.name,
+          suggestion: deHungarianize(param.name)
+        });
+      }
+    }
+  }
+
+  //--------------------------------------------------------------------------
+  // Public
+  //--------------------------------------------------------------------------
+
+  return {
+    "FunctionDeclaration": checkFunction,
+    "ArrowFunctionExpression": checkFunction,
+    "FunctionExpression": checkFunction
+  };
+};
--- a/testing/taskcluster/mach_commands.py
+++ b/testing/taskcluster/mach_commands.py
@@ -105,33 +105,27 @@ def decorate_task_treeherder_routes(task
     if 'routes' not in task:
         task['routes'] = []
 
     treeheder_env = task['extra'].get('treeherderEnv', ['staging'])
 
     for env in treeheder_env:
         task['routes'].append('{}.{}'.format(TREEHERDER_ROUTES[env], suffix))
 
-def decorate_task_json_routes(build, task, json_routes, parameters):
+def decorate_task_json_routes(task, json_routes, parameters):
     """
     Decorate the given task with routes.json routes.
 
     :param dict task: task definition.
     :param json_routes: the list of routes to use from routes.json
     :param parameters: dictionary of parameters to use in route templates
     """
-    fmt = parameters.copy()
-    fmt.update({
-        'build_product': task['extra']['build_product'],
-        'build_name': build['build_name'],
-        'build_type': build['build_type'],
-    })
     routes = task.get('routes', [])
     for route in json_routes:
-        routes.append(route.format(**fmt))
+        routes.append(route.format(**parameters))
 
     task['routes'] = routes
 
 def configure_dependent_task(task_path, parameters, taskid, templates, build_treeherder_config):
     """
     Configure a build dependent task. This is shared between post-build and test tasks.
 
     :param task_path: location to the task yaml
@@ -431,27 +425,34 @@ class Graph(object):
 
         all_routes = {}
 
         for build in job_graph:
             interactive = cmdline_interactive or build["interactive"]
             build_parameters = dict(parameters)
             build_parameters['build_slugid'] = slugid()
             build_task = templates.load(build['task'], build_parameters)
+
+            # Copy build_* attributes to expose them to post-build tasks
+            # as well as json routes and tests
+            task_extra = build_task['task']['extra']
+            build_parameters['build_name'] = task_extra['build_name']
+            build_parameters['build_type'] = task_extra['build_type']
+            build_parameters['build_product'] = task_extra['build_product']
+
             set_interactive_task(build_task, interactive)
 
             # try builds don't use cache
             if project == "try":
                 remove_caches_from_task(build_task)
 
             if params['revision_hash']:
                 decorate_task_treeherder_routes(build_task['task'],
                                                 treeherder_route)
-                decorate_task_json_routes(build,
-                                          build_task['task'],
+                decorate_task_json_routes(build_task['task'],
                                           json_routes,
                                           build_parameters)
 
             # Ensure each build graph is valid after construction.
             taskcluster_graph.build_task.validate(build_task)
             graph['tasks'].append(build_task)
 
             test_packages_url, tests_url, mozharness_url = None, None, None
--- a/testing/taskcluster/tasks/build.yml
+++ b/testing/taskcluster/tasks/build.yml
@@ -52,14 +52,16 @@ task:
       GECKO_HEAD_REPOSITORY: '{{head_repository}}'
       GECKO_HEAD_REV: '{{head_rev}}'
       GECKO_HEAD_REF: '{{head_ref}}'
       TOOLTOOL_REPO: 'https://git.mozilla.org/build/tooltool.git'
       TOOLTOOL_REV: 'master'
 
   extra:
     build_product: '{{build_product}}'
+    build_name: '{{build_name}}'
+    build_type: '{{build_type}}'
     index:
       rank: {{pushlog_id}}
     treeherder:
       groupSymbol: tc
       groupName: Submitted by taskcluster
       symbol: B
--- a/testing/taskcluster/tasks/phone_build.yml
+++ b/testing/taskcluster/tasks/phone_build.yml
@@ -52,14 +52,16 @@ task:
       # Common environment variables for checking out gecko
       GECKO_BASE_REPOSITORY: '{{base_repository}}'
       GECKO_HEAD_REPOSITORY: '{{head_repository}}'
       GECKO_HEAD_REV: '{{head_rev}}'
       GECKO_HEAD_REF: '{{head_ref}}'
 
   extra:
     build_product: 'b2g'
+    build_name: '{{build_name}}'
+    build_type: '{{build_type}}'
     index:
       rank: {{pushlog_id}}
     treeherder:
       groupSymbol: tc
       groupName: Submitted by taskcluster
       symbol: B
--- a/testing/taskcluster/tasks/post-builds/mulet_simulator.yml
+++ b/testing/taskcluster/tasks/post-builds/mulet_simulator.yml
@@ -12,16 +12,19 @@ task:
     description: 'Firefox OS Simulator addon'
   tags:
     createdForUser: {{owner}}
 
   workerType: b2gbuild
   provisionerId: aws-provisioner-v1
   schedulerId: task-graph-scheduler
 
+  routes:
+    - 'index.gecko.v1.{{project}}.latest.simulator.{{build_type}}'
+
   scopes:
     - 'docker-worker:cache:tc-vcs'
     - 'docker-worker:image:{{#docker_image}}builder{{/docker_image}}'
     - 'queue:define-task:aws-provisioner-v1/build-c4-2xlarge'
     - 'queue:create-task:aws-provisioner-v1/build-c4-2xlarge'
 
   payload:
     image: '{{#docker_image}}builder{{/docker_image}}'
rename from toolkit/components/workerlz4/lz4.cpp
rename to toolkit/components/lz4/lz4.cpp
rename from toolkit/components/workerlz4/lz4.js
rename to toolkit/components/lz4/lz4.js
--- a/toolkit/components/workerlz4/lz4.js
+++ b/toolkit/components/lz4/lz4.js
@@ -1,23 +1,33 @@
 /* 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";
 
+var SharedAll;
+var Primitives;
 if (typeof Components != "undefined") {
-  throw new Error("This file is meant to be loaded in a worker");
+  let Cu = Components.utils;
+  SharedAll = {};
+  Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", SharedAll);
+  Cu.import("resource://gre/modules/lz4_internal.js");
+  Cu.import("resource://gre/modules/ctypes.jsm");
+
+  this.EXPORTED_SYMBOLS = [
+    "Lz4"
+  ];
+  this.exports = {};
+} else if (typeof module != "undefined" && typeof require != "undefined") {
+  SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
+  Primitives = require("resource://gre/modules/lz4_internal.js");
+} else {
+  throw new Error("Please load this module with Component.utils.import or with require()");
 }
-if (!module || !exports) {
-  throw new Error("Please load this module with require()");
-}
-
-const SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
-const Internals = require("resource://gre/modules/workers/lz4_internal.js");
 
 const MAGIC_NUMBER = new Uint8Array([109, 111, 122, 76, 122, 52, 48, 0]); // "mozLz4a\0"
 
 const BYTES_IN_SIZE_HEADER = ctypes.uint32_t.size;
 
 const HEADER_SIZE = MAGIC_NUMBER.byteLength + BYTES_IN_SIZE_HEADER;
 
 const EXPECTED_HEADER_TYPE = new ctypes.ArrayType(ctypes.uint8_t, HEADER_SIZE);
@@ -71,22 +81,22 @@ function compressFileContent(array, opti
   let inputBytes;
   if (SharedAll.isTypedArray(array) && !(options && "bytes" in options)) {
     inputBytes = array.byteLength;
   } else if (options && options.bytes) {
     inputBytes = options.bytes;
   } else {
     throw new TypeError("compressFileContent requires a size");
   }
-  let maxCompressedSize = Internals.maxCompressedSize(inputBytes);
+  let maxCompressedSize = Primitives.maxCompressedSize(inputBytes);
   let outputArray = new Uint8Array(HEADER_SIZE + maxCompressedSize);
 
   // Compress to output array
   let payload = new Uint8Array(outputArray.buffer, outputArray.byteOffset + HEADER_SIZE);
-  let compressedSize = Internals.compress(array, inputBytes, payload);
+  let compressedSize = Primitives.compress(array, inputBytes, payload);
 
   // Add headers
   outputArray.set(MAGIC_NUMBER);
   let view = new DataView(outputArray.buffer);
   view.setUint32(MAGIC_NUMBER.byteLength, inputBytes, true);
 
   return new Uint8Array(outputArray.buffer, 0, HEADER_SIZE + compressedSize);
 }
@@ -120,17 +130,24 @@ function decompressFileContent(array, op
   // Prepare the input buffer
   let inputData = new DataView(array.buffer, HEADER_SIZE);
 
   // Prepare the output buffer
   let outputBuffer = new Uint8Array(expectDecompressedSize);
   let decompressedBytes = (new SharedAll.Type.size_t.implementation(0));
 
   // Decompress
-  let success = Internals.decompress(inputData, bytes - HEADER_SIZE,
-                                     outputBuffer, outputBuffer.byteLength,
-                                     decompressedBytes.address());
+  let success = Primitives.decompress(inputData, bytes - HEADER_SIZE,
+                                      outputBuffer, outputBuffer.byteLength,
+                                      decompressedBytes.address());
   if (!success) {
     throw new LZError("decompress", "becauseLZInvalidContent", "Invalid content:Decompression stopped at " + decompressedBytes.value);
   }
   return new Uint8Array(outputBuffer.buffer, outputBuffer.byteOffset, decompressedBytes.value);
 }
 exports.decompressFileContent = decompressFileContent;
+
+if (typeof Components != "undefined") {
+  this.Lz4 = {
+    compressFileContent: compressFileContent,
+    decompressFileContent: decompressFileContent
+  };
+}
rename from toolkit/components/workerlz4/lz4_internal.js
rename to toolkit/components/lz4/lz4_internal.js
--- a/toolkit/components/workerlz4/lz4_internal.js
+++ b/toolkit/components/lz4/lz4_internal.js
@@ -1,27 +1,36 @@
 /* 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";
 
+var Primitives = {};
+
+var SharedAll;
 if (typeof Components != "undefined") {
-  throw new Error("This file is meant to be loaded in a worker");
-}
-if (!module || !exports) {
-  throw new Error("Please load this module with require()");
+  let Cu = Components.utils;
+  SharedAll = {};
+  Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", SharedAll);
+
+  this.EXPORTED_SYMBOLS = [
+    "Primitives"
+  ];
+  this.Primitives = Primitives;
+  this.exports = {};
+} else if (typeof module != "undefined" && typeof require != "undefined") {
+  SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
+} else {
+  throw new Error("Please load this module with Component.utils.import or with require()");
 }
 
-var SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
 var libxul = new SharedAll.Library("libxul", SharedAll.Constants.Path.libxul);
 var Type = SharedAll.Type;
 
-var Primitives = {};
-
 libxul.declareLazyFFI(Primitives, "compress",
   "workerlz4_compress",
   null,
   /*return*/ Type.size_t,
   /*const source*/ Type.void_t.in_ptr,
   /*inputSize*/ Type.size_t,
   /*dest*/ Type.void_t.out_ptr
 );
@@ -39,19 +48,21 @@ libxul.declareLazyFFI(Primitives, "decom
 
 libxul.declareLazyFFI(Primitives, "maxCompressedSize",
   "workerlz4_maxCompressedSize",
   null,
   /*return*/ Type.size_t,
   /*inputSize*/ Type.size_t
 );
 
-module.exports = {
-  get compress() {
-    return Primitives.compress;
-  },
-  get decompress() {
-    return Primitives.decompress;
-  },
-  get maxCompressedSize() {
-    return Primitives.maxCompressedSize;
-  }
-};
+if (typeof module != "undefined") {
+  module.exports = {
+    get compress() {
+      return Primitives.compress;
+    },
+    get decompress() {
+      return Primitives.decompress;
+    },
+    get maxCompressedSize() {
+      return Primitives.maxCompressedSize;
+    }
+  };
+}
rename from toolkit/components/workerlz4/moz.build
rename to toolkit/components/lz4/moz.build
--- a/toolkit/components/workerlz4/moz.build
+++ b/toolkit/components/lz4/moz.build
@@ -1,17 +1,17 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
 
-EXTRA_JS_MODULES.workers += [
+EXTRA_JS_MODULES += [
     'lz4.js',
     'lz4_internal.js',
 ]
 
 SOURCES += [
     'lz4.cpp',
 ]
 
rename from toolkit/components/workerlz4/tests/xpcshell/data/chrome.manifest
rename to toolkit/components/lz4/tests/xpcshell/data/chrome.manifest
rename from toolkit/components/workerlz4/tests/xpcshell/data/compression.lz
rename to toolkit/components/lz4/tests/xpcshell/data/compression.lz
rename from toolkit/components/workerlz4/tests/xpcshell/data/worker_lz4.js
rename to toolkit/components/lz4/tests/xpcshell/data/worker_lz4.js
--- a/toolkit/components/workerlz4/tests/xpcshell/data/worker_lz4.js
+++ b/toolkit/components/lz4/tests/xpcshell/data/worker_lz4.js
@@ -39,18 +39,18 @@ self.onmessage = function() {
     dump("Full stack: " + moduleStack + "\n");
     throw error;
   }
 };
 
 var Lz4;
 var Internals;
 function test_import() {
-  Lz4 = require("resource://gre/modules/workers/lz4.js");
-  Internals = require("resource://gre/modules/workers/lz4_internal.js");
+  Lz4 = require("resource://gre/modules/lz4.js");
+  Internals = require("resource://gre/modules/lz4_internal.js");
 }
 
 function test_bound() {
   for (let k of ["compress", "decompress", "maxCompressedSize"]) {
     try {
       do_print("Checking the existence of " + k + "\n");
       do_check_true(!!Internals[k]);
       do_print(k + " exists");
rename from toolkit/components/workerlz4/tests/xpcshell/test_lz4.js
rename to toolkit/components/lz4/tests/xpcshell/test_lz4.js
new file mode 100644
--- /dev/null
+++ b/toolkit/components/lz4/tests/xpcshell/test_lz4_sync.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const Cu = Components.utils;
+Cu.import("resource://gre/modules/lz4.js");
+Cu.import("resource://gre/modules/osfile.jsm");
+
+function run_test() {
+  run_next_test();
+}
+
+function compare_arrays(a, b) {
+  return Array.prototype.join.call(a) == Array.prototype.join.call(a);
+}
+
+add_task(function() {
+  let path = OS.Path.join("data", "compression.lz");
+  let data = yield OS.File.read(path);
+  let decompressed = Lz4.decompressFileContent(data);
+  let text = (new TextDecoder()).decode(decompressed);
+  do_check_eq(text, "Hello, lz4");
+});
+
+add_task(function() {
+  for (let length of [0, 1, 1024]) {
+    let array = new Uint8Array(length);
+    for (let i = 0; i < length; ++i) {
+      array[i] = i % 256;
+    }
+
+    let compressed = Lz4.compressFileContent(array);
+    do_print("Compressed " + array.byteLength + " bytes into " +
+             compressed.byteLength);
+
+    let decompressed = Lz4.decompressFileContent(compressed);
+    do_print("Decompressed " + compressed.byteLength + " bytes into " +
+             decompressed.byteLength);
+
+    do_check_true(compare_arrays(array, decompressed));
+  }
+});
rename from toolkit/components/workerlz4/tests/xpcshell/xpcshell.ini
rename to toolkit/components/lz4/tests/xpcshell/xpcshell.ini
--- a/toolkit/components/workerlz4/tests/xpcshell/xpcshell.ini
+++ b/toolkit/components/lz4/tests/xpcshell/xpcshell.ini
@@ -3,8 +3,9 @@ head =
 tail =
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 support-files =
   data/worker_lz4.js
   data/chrome.manifest
   data/compression.lz
 
 [test_lz4.js]
+[test_lz4_sync.js]
--- a/toolkit/components/moz.build
+++ b/toolkit/components/moz.build
@@ -26,16 +26,17 @@ DIRS += [
     'exthelper',
     'filepicker',
     'filewatcher',
     'finalizationwitness',
     'formautofill',
     'find',
     'gfx',
     'jsdownloads',
+    'lz4',
     'mediasniffer',
     'microformats',
     'osfile',
     'parentalcontrols',
     'passwordmgr',
     'perf',
     'places',
     'privatebrowsing',
@@ -51,17 +52,16 @@ DIRS += [
     'telemetry',
     'thumbnails',
     'timermanager',
     'typeaheadfind',
     'utils',
     'urlformatter',
     'viewconfig',
     'workerloader',
-    'workerlz4',
     'xulstore'
 ]
 
 if CONFIG['MOZ_BUILD_APP'] != 'mobile/android':
     DIRS += ['viewsource'];
 
     if CONFIG['NS_PRINTING']:
         DIRS += ['printing']
--- a/toolkit/components/osfile/modules/osfile_shared_front.jsm
+++ b/toolkit/components/osfile/modules/osfile_shared_front.jsm
@@ -13,17 +13,17 @@ if (typeof Components != "undefined") {
   throw new Error("osfile_shared_front.jsm cannot be used from the main thread");
 }
 (function(exports) {
 
 var SharedAll =
   require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
 var Path = require("resource://gre/modules/osfile/ospath.jsm");
 var Lz4 =
-  require("resource://gre/modules/workers/lz4.js");
+  require("resource://gre/modules/lz4.js");
 var LOG = SharedAll.LOG.bind(SharedAll, "Shared front-end");
 var clone = SharedAll.clone;
 
 /**
  * Code shared by implementations of File.
  *
  * @param {*} fd An OS-specific file handle.
  * @param {string} path File path of the file handle, used for error-reporting.
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -4347,20 +4347,23 @@
     "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
     "expires_in_version": "default",
     "kind": "exponential",
     "high": "3000",
     "n_buckets": 10,
     "extended_statistics_ok": true,
     "description": "Session restore: Time spent blocking the main thread while restoring a window state (ms)"
   },
-  "FX_TABLET_MODE_USED_DURING_SESSION": {
-    "expires_in_version": "46",
-    "kind": "count",
-    "description": "Windows 10+ only: The number of times tablet-mode is used during a session"
+  "FX_TABLETMODE_PAGE_LOAD": {
+    "expires_in_version": "47",
+    "kind": "exponential",
+    "high": 100000,
+    "n_buckets": 30,
+    "keyed": true,
+    "description": "Number of toplevel location changes in tablet and desktop mode (only used on win10 where tablet mode is available)"
   },
   "FX_TOUCH_USED": {
     "expires_in_version": "46",
     "kind": "count",
     "description": "Windows only. Counts occurrences of touch events"
   },
   "FX_URLBAR_SELECTED_RESULT_INDEX": {
     "expires_in_version": "45",
@@ -5700,23 +5703,43 @@
     "kind": "boolean",
     "description": "Count the number of times the user clicked 'block' on the hidden-plugin infobar."
   },
   "PLUGINS_INFOBAR_ALLOW": {
     "expires_in_version": "never",
     "kind": "boolean",
     "description": "Count the number of times the user clicked 'allow' on the hidden-plugin infobar."
   },
-  "POPUP_NOTIFICATION_MAINACTION_TRIGGERED_MS": {
-    "expires_in_version": "40",
-    "kind": "linear",
-    "low": 25,
-    "high": "80 * 25",
-    "n_buckets": "80 + 1",
-    "description": "The time (in milliseconds) after showing a PopupNotification that the mainAction was first triggered"
+  "POPUP_NOTIFICATION_STATS": {
+    "alert_emails": ["firefox-dev@mozilla.org"],
+    "expires_in_version": "48",
+    "kind": "enumerated",
+    "keyed": true,
+    "n_values": 40,
+    "description": "(Bug 1207089) Usage of popup notifications, keyed by ID (0 = Offered, 1..4 = Action, 5 = Click outside, 6 = Leave page, 7 = Use 'X', 8 = Not now, 10 = Open submenu, 11 = Learn more. Add 20 if happened after reopen.)"
+  },
+  "POPUP_NOTIFICATION_MAIN_ACTION_MS": {
+    "alert_emails": ["firefox-dev@mozilla.org"],
+    "expires_in_version": "48",
+    "kind": "exponential",
+    "keyed": true,
+    "low": 100,
+    "high": 600000,
+    "n_buckets": 40,
+    "description": "(Bug 1207089) Time in ms between initially requesting a popup notification and triggering the main action, keyed by ID"
+  },
+  "POPUP_NOTIFICATION_DISMISSAL_MS": {
+    "alert_emails": ["firefox-dev@mozilla.org"],
+    "expires_in_version": "48",
+    "kind": "exponential",
+    "keyed": true,
+    "low": 200,
+    "high": 20000,
+    "n_buckets": 50,
+    "description": "(Bug 1207089) Time in ms between displaying a popup notification and dismissing it without an action the first time, keyed by ID"
   },
   "DEVTOOLS_DEBUGGER_RDP_LOCAL_RELOAD_MS": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": "10000",
     "n_buckets": "1000",
     "description": "The time (in milliseconds) that it took a 'reload' request to go round trip."
   },
--- a/toolkit/content/widgets/notification.xml
+++ b/toolkit/content/widgets/notification.xml
@@ -487,25 +487,25 @@
           </xul:vbox>
           <xul:toolbarbutton anonid="closebutton"
                              class="messageCloseButton close-icon popup-notification-closebutton tabbable"
                              xbl:inherits="oncommand=closebuttoncommand"
                              tooltiptext="&closeNotification.tooltip;"/>
         </xul:hbox>
         <children includes="popupnotificationcontent"/>
         <xul:label class="text-link popup-notification-learnmore-link"
-               xbl:inherits="href=learnmoreurl">&learnMore;</xul:label>
+               xbl:inherits="onclick=learnmoreclick,href=learnmoreurl">&learnMore;</xul:label>
         <xul:spacer flex="1"/>
         <xul:hbox class="popup-notification-button-container"
                   pack="end" align="center">
           <children includes="button"/>
           <xul:button anonid="button"
                       class="popup-notification-menubutton"
                       type="menu-button"
-                      xbl:inherits="oncommand=buttoncommand,label=buttonlabel,accesskey=buttonaccesskey">
+                      xbl:inherits="oncommand=buttoncommand,onpopupshown=buttonpopupshown,label=buttonlabel,accesskey=buttonaccesskey">
             <xul:menupopup anonid="menupopup"
                            xbl:inherits="oncommand=menucommand">
               <children/>
               <xul:menuitem class="menuitem-iconic popup-notification-closeitem"
                             label="&closeNotificationItem.label;"
                             xbl:inherits="oncommand=closeitemcommand,hidden=hidenotnow"/>
             </xul:menupopup>
           </xul:button>
--- a/toolkit/modules/PopupNotifications.jsm
+++ b/toolkit/modules/PopupNotifications.jsm
@@ -2,30 +2,46 @@
  * 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/. */
 
 this.EXPORTED_SYMBOLS = ["PopupNotifications"];
 
 var Cc = Components.classes, Ci = Components.interfaces, Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 
 const NOTIFICATION_EVENT_DISMISSED = "dismissed";
 const NOTIFICATION_EVENT_REMOVED = "removed";
 const NOTIFICATION_EVENT_SHOWING = "showing";
 const NOTIFICATION_EVENT_SHOWN = "shown";
 const NOTIFICATION_EVENT_SWAPPING = "swapping";
 
 const ICON_SELECTOR = ".notification-anchor-icon";
 const ICON_ATTRIBUTE_SHOWING = "showing";
 const ICON_ANCHOR_ATTRIBUTE = "popupnotificationanchor";
 
 const PREF_SECURITY_DELAY = "security.notification_enable_delay";
 
+// Enumerated values for the POPUP_NOTIFICATION_STATS telemetry histogram.
+const TELEMETRY_STAT_OFFERED = 0;
+const TELEMETRY_STAT_ACTION_1 = 1;
+const TELEMETRY_STAT_ACTION_2 = 2;
+const TELEMETRY_STAT_ACTION_3 = 3;
+const TELEMETRY_STAT_ACTION_LAST = 4;
+const TELEMETRY_STAT_DISMISSAL_CLICK_ELSEWHERE = 5;
+const TELEMETRY_STAT_DISMISSAL_LEAVE_PAGE = 6;
+const TELEMETRY_STAT_DISMISSAL_CLOSE_BUTTON = 7;
+const TELEMETRY_STAT_DISMISSAL_NOT_NOW = 8;
+const TELEMETRY_STAT_OPEN_SUBMENU = 10;
+const TELEMETRY_STAT_LEARN_MORE = 11;
+
+const TELEMETRY_STAT_REOPENED_OFFSET = 20;
+
 var popupNotificationsMap = new WeakMap();
 var gNotificationParents = new WeakMap;
 
 function getAnchorFromBrowser(aBrowser, aAnchorID) {
   let attrPrefix = aAnchorID ? aAnchorID.replace("notification-icon", "") : "";
   let anchor = aBrowser.getAttribute(attrPrefix + ICON_ANCHOR_ATTRIBUTE) ||
                aBrowser[attrPrefix + ICON_ANCHOR_ATTRIBUTE] ||
                aBrowser.getAttribute(ICON_ANCHOR_ATTRIBUTE) ||
@@ -49,31 +65,52 @@ function Notification(id, message, ancho
   this.id = id;
   this.message = message;
   this.anchorID = anchorID;
   this.mainAction = mainAction;
   this.secondaryActions = secondaryActions || [];
   this.browser = browser;
   this.owner = owner;
   this.options = options || {};
+
+  this._dismissed = false;
+  this.wasDismissed = false;
+  this.recordedTelemetryStats = new Set();
+  this.isPrivate = PrivateBrowsingUtils.isWindowPrivate(
+                                        this.browser.ownerDocument.defaultView);
+  this.timeCreated = this.owner.window.performance.now();
 }
 
 Notification.prototype = {
 
   id: null,
   message: null,
   anchorID: null,
   mainAction: null,
   secondaryActions: null,
   browser: null,
   owner: null,
   options: null,
   timeShown: null,
 
   /**
+   * Indicates whether the notification is currently dismissed.
+   */
+  set dismissed(value) {
+    this._dismissed = value;
+    if (value) {
+      // Keep the dismissal into account when recording telemetry.
+      this.wasDismissed = true;
+    }
+  },
+  get dismissed() {
+    return this._dismissed;
+  },
+
+  /**
    * Removes the notification and updates the popup accordingly if needed.
    */
   remove: function Notification_remove() {
     this.owner.remove(this);
   },
 
   get anchorElement() {
     let iconBox = this.owner.iconBox;
@@ -90,17 +127,55 @@ Notification.prototype = {
       anchorElement = iconBox.querySelector("#default-notification-icon") ||
                       iconBox;
 
     return anchorElement;
   },
 
   reshow: function() {
     this.owner._reshowNotifications(this.anchorElement, this.browser);
-  }
+  },
+
+  /**
+   * Adds a value to the specified histogram, that must be keyed by ID.
+   */
+  _recordTelemetry(histogramId, value) {
+    if (this.isPrivate) {
+      // The reason why we don't record telemetry in private windows is because
+      // the available actions can be different from regular mode. The main
+      // difference is that all of the persistent permission options like
+      // "Always remember" aren't there, so they really need to be handled
+      // separately to avoid skewing results. For notifications with the same
+      // choices, there would be no reason not to record in private windows as
+      // well, but it's just simpler to use the same check for everything.
+      return;
+    }
+    let histogram = Services.telemetry.getKeyedHistogramById(histogramId);
+    histogram.add("(all)", value);
+    histogram.add(this.id, value);
+  },
+
+  /**
+   * Adds an enumerated value to the POPUP_NOTIFICATION_STATS histogram,
+   * ensuring that it is recorded at most once for each distinct Notification.
+   *
+   * Statistics for reopened notifications are recorded in separate buckets.
+   *
+   * @param value
+   *        One of the TELEMETRY_STAT_ constants.
+   */
+  _recordTelemetryStat(value) {
+    if (this.wasDismissed) {
+      value += TELEMETRY_STAT_REOPENED_OFFSET;
+    }
+    if (!this.recordedTelemetryStats.has(value)) {
+      this.recordedTelemetryStats.add(value);
+      this._recordTelemetry("POPUP_NOTIFICATION_STATS", value);
+    }
+  },
 };
 
 /**
  * The PopupNotifications object manages popup notifications for a given browser
  * window.
  * @param tabbrowser
  *        window's <xul:tabbrowser/>. Used to observe tab switching events and
  *        for determining the active browser element.
@@ -411,16 +486,22 @@ PopupNotifications.prototype = {
   handleEvent: function (aEvent) {
     switch (aEvent.type) {
       case "popuphidden":
         this._onPopupHidden(aEvent);
         break;
       case "activate":
       case "TabSelect":
         let self = this;
+        // This is where we could detect if the panel is dismissed if the page
+        // was switched. Unfortunately, the user usually has clicked elsewhere
+        // at this point so this value only gets recorded for programmatic
+        // reasons, like the "Learn More" link being clicked and resulting in a
+        // tab switch.
+        this.nextDismissReason = TELEMETRY_STAT_DISMISSAL_LEAVE_PAGE;
         // setTimeout(..., 0) needed, otherwise openPopup from "activate" event
         // handler results in the popup being hidden again for some reason...
         this.window.setTimeout(function () {
           self._update();
         }, 0);
         break;
       case "click":
       case "keypress":
@@ -460,17 +541,21 @@ PopupNotifications.prototype = {
     // remove the notification
     notifications.splice(index, 1);
     this._fireCallback(notification, NOTIFICATION_EVENT_REMOVED);
   },
 
   /**
    * Dismisses the notification without removing it.
    */
-  _dismiss: function PopupNotifications_dismiss() {
+  _dismiss: function PopupNotifications_dismiss(telemetryReason) {
+    if (telemetryReason) {
+      this.nextDismissReason = telemetryReason;
+    }
+
     let browser = this.panel.firstChild &&
                   this.panel.firstChild.notification.browser;
     this.panel.hidePopup();
     if (browser)
       browser.focus();
   },
 
   /**
@@ -541,27 +626,31 @@ PopupNotifications.prototype = {
       if (popupnotification)
         gNotificationParents.set(popupnotification, popupnotification.parentNode);
       else
         popupnotification = doc.createElementNS(XUL_NS, "popupnotification");
 
       popupnotification.setAttribute("label", n.message);
       popupnotification.setAttribute("id", popupnotificationID);
       popupnotification.setAttribute("popupid", n.id);
-      popupnotification.setAttribute("closebuttoncommand", "PopupNotifications._dismiss();");
+      popupnotification.setAttribute("closebuttoncommand", `PopupNotifications._dismiss(${TELEMETRY_STAT_DISMISSAL_CLOSE_BUTTON});`);
       if (n.mainAction) {
         popupnotification.setAttribute("buttonlabel", n.mainAction.label);
         popupnotification.setAttribute("buttonaccesskey", n.mainAction.accessKey);
-        popupnotification.setAttribute("buttoncommand", "PopupNotifications._onButtonCommand(event);");
+        popupnotification.setAttribute("buttoncommand", "PopupNotifications._onButtonEvent(event, 'buttoncommand');");
+        popupnotification.setAttribute("buttonpopupshown", "PopupNotifications._onButtonEvent(event, 'buttonpopupshown');");
+        popupnotification.setAttribute("learnmoreclick", "PopupNotifications._onButtonEvent(event, 'learnmoreclick');");
         popupnotification.setAttribute("menucommand", "PopupNotifications._onMenuCommand(event);");
-        popupnotification.setAttribute("closeitemcommand", "PopupNotifications._dismiss();event.stopPropagation();");
+        popupnotification.setAttribute("closeitemcommand", `PopupNotifications._dismiss(${TELEMETRY_STAT_DISMISSAL_NOT_NOW});event.stopPropagation();`);
       } else {
         popupnotification.removeAttribute("buttonlabel");
         popupnotification.removeAttribute("buttonaccesskey");
         popupnotification.removeAttribute("buttoncommand");
+        popupnotification.removeAttribute("buttonpopupshown");
+        popupnotification.removeAttribute("learnmoreclick");
         popupnotification.removeAttribute("menucommand");
         popupnotification.removeAttribute("closeitemcommand");
       }
 
       if (n.options.popupIconURL)
         popupnotification.setAttribute("icon", n.options.popupIconURL);
 
       if (n.options.learnMoreURL)
@@ -583,24 +672,33 @@ PopupNotifications.prototype = {
           popupnotification.removeAttribute("origin");
         }
       } else
         popupnotification.removeAttribute("origin");
 
       popupnotification.notification = n;
 
       if (n.secondaryActions) {
+        let telemetryStatId = TELEMETRY_STAT_ACTION_2;
+
         n.secondaryActions.forEach(function (a) {
           let item = doc.createElementNS(XUL_NS, "menuitem");
           item.setAttribute("label", a.label);
           item.setAttribute("accesskey", a.accessKey);
           item.notification = n;
           item.action = a;
 
           popupnotification.appendChild(item);
+
+          // We can only record a limited number of actions in telemetry. If
+          // there are more, the latest are all recorded in the last bucket.
+          item.action.telemetryStatId = telemetryStatId;
+          if (telemetryStatId < TELEMETRY_STAT_ACTION_LAST) {
+            telemetryStatId++;
+          }
         }, this);
 
         if (n.options.hideNotNow) {
           popupnotification.setAttribute("hidenotnow", "true");
         }
         else if (n.secondaryActions.length) {
           let closeItemSeparator = doc.createElementNS(XUL_NS, "menuseparator");
           popupnotification.appendChild(closeItemSeparator);
@@ -653,19 +751,28 @@ PopupNotifications.prototype = {
       }
 
       this._currentAnchorElement = anchorElement;
 
       // On OS X and Linux we need a different panel arrow color for
       // click-to-play plugins, so copy the popupid and use css.
       this.panel.setAttribute("popupid", this.panel.firstChild.getAttribute("popupid"));
       notificationsToShow.forEach(function (n) {
+        // Record that the notification was actually displayed on screen.
+        // Notifications that were opened a second time or that were originally
+        // shown with "options.dismissed" will be recorded in a separate bucket.
+        n._recordTelemetryStat(TELEMETRY_STAT_OFFERED);
         // Remember the time the notification was shown for the security delay.
         n.timeShown = this.window.performance.now();
       }, this);
+
+      // Unless the panel closing is triggered by a specific known code path,
+      // the next reason will be that the user clicked elsewhere.
+      this.nextDismissReason = TELEMETRY_STAT_DISMISSAL_CLICK_ELSEWHERE;
+
       this.panel.openPopup(anchorElement, "bottomcenter topleft");
       notificationsToShow.forEach(function (n) {
         this._fireCallback(n, NOTIFICATION_EVENT_SHOWN);
       }, this);
       // This notification is used by tests to know when all the processing
       // required to display the panel has happened.
       this.panel.dispatchEvent(new this.window.CustomEvent("Shown"));
     });
@@ -974,60 +1081,85 @@ PopupNotifications.prototype = {
     let notifications = this._getNotificationsForBrowser(browser);
     // Mark notifications as dismissed and call dismissal callbacks
     Array.forEach(this.panel.childNodes, function (nEl) {
       let notificationObj = nEl.notification;
       // Never call a dismissal handler on a notification that's been removed.
       if (notifications.indexOf(notificationObj) == -1)
         return;
 
+      // Record the time of the first notification dismissal if the main action
+      // was not triggered in the meantime.
+      let timeSinceShown = this.window.performance.now() - notificationObj.timeShown;
+      if (!notificationObj.wasDismissed &&
+          !notificationObj.recordedTelemetryMainAction) {
+        notificationObj._recordTelemetry("POPUP_NOTIFICATION_DISMISSAL_MS",
+                                         timeSinceShown);
+      }
+      notificationObj._recordTelemetryStat(this.nextDismissReason);
+
       // Do not mark the notification as dismissed or fire NOTIFICATION_EVENT_DISMISSED
       // if the notification is removed.
       if (notificationObj.options.removeOnDismissal) {
         this._remove(notificationObj);
       } else {
         notificationObj.dismissed = true;
         this._fireCallback(notificationObj, NOTIFICATION_EVENT_DISMISSED);
       }
     }, this);
   },
 
-  _onButtonCommand: function PopupNotifications_onButtonCommand(event) {
+  _onButtonEvent(event, type) {
     // Need to find the associated notification object, which is a bit tricky
     // since it isn't associated with the button directly - this is kind of
     // gross and very dependent on the structure of the popupnotification
     // binding's content.
     let target = event.originalTarget;
     let notificationEl;
     let parent = target;
     while (parent && (parent = target.ownerDocument.getBindingParent(parent)))
       notificationEl = parent;
 
     if (!notificationEl)
-      throw "PopupNotifications_onButtonCommand: couldn't find notification element";
+      throw "PopupNotifications._onButtonEvent: couldn't find notification element";
 
     if (!notificationEl.notification)
-      throw "PopupNotifications_onButtonCommand: couldn't find notification";
+      throw "PopupNotifications._onButtonEvent: couldn't find notification";
 
     let notification = notificationEl.notification;
-    let timeSinceShown = this.window.performance.now() - notification.timeShown;
 
-    // Only report the first time mainAction is triggered and remember that this occurred.
-    if (!notification.timeMainActionFirstTriggered) {
-      notification.timeMainActionFirstTriggered = timeSinceShown;
-      Services.telemetry.getHistogramById("POPUP_NOTIFICATION_MAINACTION_TRIGGERED_MS").
-                         add(timeSinceShown);
+    if (type == "buttonpopupshown") {
+      notification._recordTelemetryStat(TELEMETRY_STAT_OPEN_SUBMENU);
+      return;
+    }
+
+    if (type == "learnmoreclick") {
+      notification._recordTelemetryStat(TELEMETRY_STAT_LEARN_MORE);
+      return;
     }
 
+    // Record the total timing of the main action since the notification was
+    // created, even if the notification was dismissed in the meantime.
+    let timeSinceCreated = this.window.performance.now() - notification.timeCreated;
+    if (!notification.recordedTelemetryMainAction) {
+      notification.recordedTelemetryMainAction = true;
+      notification._recordTelemetry("POPUP_NOTIFICATION_MAIN_ACTION_MS",
+                                    timeSinceCreated);
+    }
+
+    let timeSinceShown = this.window.performance.now() - notification.timeShown;
     if (timeSinceShown < this.buttonDelay) {
-      Services.console.logStringMessage("PopupNotifications_onButtonCommand: " +
+      Services.console.logStringMessage("PopupNotifications._onButtonEvent: " +
                                         "Button click happened before the security delay: " +
                                         timeSinceShown + "ms");
       return;
     }
+
+    notification._recordTelemetryStat(TELEMETRY_STAT_ACTION_1);
+
     try {
       notification.mainAction.callback.call();
     } catch(error) {
       Cu.reportError(error);
     }
 
     if (notification.mainAction.dismiss) {
       this._dismiss();
@@ -1039,16 +1171,19 @@ PopupNotifications.prototype = {
   },
 
   _onMenuCommand: function PopupNotifications_onMenuCommand(event) {
     let target = event.originalTarget;
     if (!target.action || !target.notification)
       throw "menucommand target has no associated action/notification";
 
     event.stopPropagation();
+
+    target.notification._recordTelemetryStat(target.action.telemetryStatId);
+
     try {
       target.action.callback.call();
     } catch(error) {
       Cu.reportError(error);
     }
 
     if (target.action.dismiss) {
       this._dismiss();
--- a/toolkit/mozapps/update/tests/TestAUSHelper.cpp
+++ b/toolkit/mozapps/update/tests/TestAUSHelper.cpp
@@ -212,17 +212,17 @@ int NS_main(int argc, NS_tchar **argv)
             "              \tline arguments.\n" \
             "\n" \
             "Note: All paths must be relative.\n" \
             "\n");
     return 1;
   }
 
   if (!NS_tstrcmp(argv[1], NS_T("check-signature"))) {
-#ifdef XP_WIN
+#if defined(XP_WIN) && defined(MOZ_MAINTENANCE_SERVICE)
     if (ERROR_SUCCESS == VerifyCertificateTrustForFile(argv[2])) {
       return 0;
     } else {
       return 1;
     }
 #else
     // Not implemented on non-Windows platforms
     return 1;
--- a/view/nsView.cpp
+++ b/view/nsView.cpp
@@ -14,16 +14,17 @@
 #include "nsIWidget.h"
 #include "nsViewManager.h"
 #include "nsIFrame.h"
 #include "nsPresArena.h"
 #include "nsXULPopupManager.h"
 #include "nsIWidgetListener.h"
 #include "nsContentUtils.h" // for nsAutoScriptBlocker
 #include "mozilla/TimelineConsumers.h"
+#include "mozilla/CompositeTimelineMarker.h"
 
 using namespace mozilla;
 
 nsView::nsView(nsViewManager* aViewManager, nsViewVisibility aVisibility)
 {
   MOZ_COUNT_CTOR(nsView);
 
   mVis = aVisibility;
@@ -1093,19 +1094,19 @@ nsView::DidCompositeWindow(const TimeSta
       return;
     }
 
     nsIDocShell* docShell = context->GetDocShell();
     RefPtr<TimelineConsumers> timelines = TimelineConsumers::Get();
 
     if (timelines && timelines->HasConsumer(docShell)) {
       timelines->AddMarkerForDocShell(docShell,
-        "Composite", aCompositeStart, MarkerTracingType::START);
+        MakeUnique<CompositeTimelineMarker>(aCompositeStart, MarkerTracingType::START));
       timelines->AddMarkerForDocShell(docShell,
-        "Composite", aCompositeEnd, MarkerTracingType::END);
+        MakeUnique<CompositeTimelineMarker>(aCompositeEnd, MarkerTracingType::END));
     }
   }
 }
 
 void
 nsView::RequestRepaint()
 {
   nsIPresShell* presShell = mViewManager->GetPresShell();