Merge m-c to b2ginbound, a=merge
authorWes Kocher <wkocher@mozilla.com>
Wed, 11 Nov 2015 17:13:06 -0800
changeset 308434 9f7af27bf22fddfd062df2b756187578e7facf8a
parent 308433 6271ec21093c3a9573216b47f02445c63ad961d6 (current diff)
parent 308382 a8ed7dd831d1969a5a1a8636e63bd93d6aeaf94a (diff)
child 308435 93d2777599e9084abd4c41364d52e147dbc27322
push id7470
push users.kaspari@gmail.com
push dateThu, 12 Nov 2015 12:51:02 +0000
reviewersmerge
milestone45.0a1
Merge m-c to b2ginbound, a=merge
devtools/client/inspector/selector-search.js
ipc/chromium/Makefile.in
ipc/glue/Makefile.in
toolkit/crashreporter/google-breakpad/src/common/mac/Makefile.in
toolkit/crashreporter/google-breakpad/src/tools/mac/dump_syms/Makefile.in
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -2831,18 +2831,17 @@ var BrowserOnClick = {
         Services.telemetry.getHistogramById("TLS_ERROR_REPORT_UI")
           .add(reportStatus);
       break;
       case "Browser:OverrideWeakCrypto":
         let weakCryptoOverride = Cc["@mozilla.org/security/weakcryptooverride;1"]
                                    .getService(Ci.nsIWeakCryptoOverride);
         weakCryptoOverride.addWeakCryptoOverride(
           msg.data.location.hostname,
-          PrivateBrowsingUtils.isBrowserPrivate(gBrowser.selectedBrowser),
-          true /* temporary */);
+          PrivateBrowsingUtils.isBrowserPrivate(gBrowser.selectedBrowser));
       break;
     }
   },
 
   onSSLErrorReport: function(browser, elementId, documentURI, location, securityInfo) {
     function showReportStatus(reportStatus) {
       gBrowser.selectedBrowser
           .messageManager
@@ -7135,16 +7134,17 @@ var gIdentityHandler = {
 
     // Then, update the user interface with the available data.
     this.refreshIdentityBlock();
     // Handle a location change while the Control Center is focused
     // by closing the popup (bug 1207542)
     if (shouldHidePopup) {
       this._identityPopup.hidePopup();
     }
+    this.showWeakCryptoInfoBar();
 
     // NOTE: We do NOT update the identity popup (the control center) when
     // we receive a new security state on the existing page (i.e. from a
     // subframe). If the user opened the popup and looks at the provided
     // information we don't want to suddenly change the panel contents.
   },
 
   /**
@@ -7284,16 +7284,65 @@ var gIdentityHandler = {
     // Set cropping and direction
     this._identityIconLabel.crop = icon_country_label ? "end" : "center";
     this._identityIconLabel.parentNode.style.direction = icon_labels_dir;
     // Hide completely if the organization label is empty
     this._identityIconLabel.parentNode.collapsed = icon_label ? false : true;
   },
 
   /**
+   * Show the weak crypto notification bar.
+   */
+  showWeakCryptoInfoBar() {
+    if (!this._uriHasHost || !this._isBroken || !this._sslStatus.cipherName ||
+        this._sslStatus.cipherName.indexOf("_RC4_") < 0) {
+      return;
+    }
+
+    let notificationBox = gBrowser.getNotificationBox();
+    let notification = notificationBox.getNotificationWithValue("weak-crypto");
+    if (notification) {
+      return;
+    }
+
+    let brandBundle = document.getElementById("bundle_brand");
+    let brandShortName = brandBundle.getString("brandShortName");
+    let message = gNavigatorBundle.getFormattedString("weakCryptoOverriding.message",
+                                                      [brandShortName]);
+
+    let host = this._uri.host;
+    let port = 443;
+    try {
+      if (this._uri.port > 0) {
+        port = this._uri.port;
+      }
+    } catch (e) {}
+
+    let buttons = [{
+      label: gNavigatorBundle.getString("revokeOverride.label"),
+      accessKey: gNavigatorBundle.getString("revokeOverride.accesskey"),
+      callback: function (aNotification, aButton) {
+        try {
+          let weakCryptoOverride = Cc["@mozilla.org/security/weakcryptooverride;1"]
+                                     .getService(Ci.nsIWeakCryptoOverride);
+          weakCryptoOverride.removeWeakCryptoOverride(host, port,
+            PrivateBrowsingUtils.isBrowserPrivate(gBrowser.selectedBrowser));
+          BrowserReloadWithFlags(nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
+        } catch (e) {
+          Cu.reportError(e);
+        }
+      }
+    }];
+
+    const priority = notificationBox.PRIORITY_WARNING_MEDIUM;
+    notificationBox.appendNotification(message, "weak-crypto", null,
+                                       priority, buttons);
+  },
+
+  /**
    * Set up the title and content messages for the identity message popup,
    * based on the specified mode, and the details of the SSL cert, where
    * applicable
    */
   refreshIdentityPopup() {
     // Update "Learn More" for Mixed Content Blocking and Insecure Login Forms.
     let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
     this._identityPopupMixedContentLearnMore
--- a/browser/base/content/test/general/browser_insecureLoginForms.js
+++ b/browser/base/content/test/general/browser_insecureLoginForms.js
@@ -1,14 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Load directly from the browser-chrome support files of login tests.
-const testUrlPath =
-      "://example.com/browser/toolkit/components/passwordmgr/test/browser/";
+const TEST_URL_PATH = "/browser/toolkit/components/passwordmgr/test/browser/";
 
 /**
  * Waits for the given number of occurrences of InsecureLoginFormsStateChange
  * on the given browser element.
  */
 function waitForInsecureLoginFormsStateChange(browser, count) {
   return BrowserTestUtils.waitForEvent(browser, "InsecureLoginFormsStateChange",
                                        false, () => --count == 0);
@@ -17,31 +16,36 @@ function waitForInsecureLoginFormsStateC
 /**
  * Checks the insecure login forms logic for the identity block.
  */
 add_task(function* test_simple() {
   yield new Promise(resolve => SpecialPowers.pushPrefEnv({
     "set": [["security.insecure_password.ui.enabled", true]],
   }, resolve));
 
-  for (let scheme of ["http", "https"]) {
-    let tab = gBrowser.addTab(scheme + testUrlPath + "form_basic.html");
+  for (let [origin, expectWarning] of [
+    ["http://example.com", true],
+    ["http://127.0.0.1", false],
+    ["https://example.com", false],
+  ]) {
+    let testUrlPath = origin + TEST_URL_PATH;
+    let tab = gBrowser.addTab(testUrlPath + "form_basic.html");
     let browser = tab.linkedBrowser;
     yield Promise.all([
       BrowserTestUtils.switchTab(gBrowser, tab),
       BrowserTestUtils.browserLoaded(browser),
       // One event is triggered by pageshow and one by DOMFormHasPassword.
       waitForInsecureLoginFormsStateChange(browser, 2),
     ]);
 
     let { gIdentityHandler } = gBrowser.ownerGlobal;
     gIdentityHandler._identityBox.click();
     document.getElementById("identity-popup-security-expander").click();
 
-    if (scheme == "http") {
+    if (expectWarning) {
       let identityBoxImage = gBrowser.ownerGlobal
             .getComputedStyle(document.getElementById("page-proxy-favicon"), "")
             .getPropertyValue("list-style-image");
       let securityViewBG = gBrowser.ownerGlobal
             .getComputedStyle(document.getElementById("identity-popup-securityView"), "")
             .getPropertyValue("background-image");
       let securityContentBG = gBrowser.ownerGlobal
             .getComputedStyle(document.getElementById("identity-popup-security-content"), "")
@@ -59,34 +63,35 @@ add_task(function* test_simple() {
                       element => !is_hidden(element)).length, 1,
          "The 'Learn more' link should be visible once.");
     }
 
     // Messages should be visible when the scheme is HTTP, and invisible when
     // the scheme is HTTPS.
     is(Array.every(document.querySelectorAll("[when-loginforms=insecure]"),
                    element => !is_hidden(element)),
-       scheme == "http",
-       "The relevant messages should visible or hidden.");
+       expectWarning,
+       "The relevant messages should be visible or hidden.");
 
     gIdentityHandler._identityPopup.hidden = true;
     gBrowser.removeTab(tab);
   }
 });
 
 /**
  * Checks that the insecure login forms logic does not regress mixed content
  * blocking messages when mixed active content is loaded.
  */
 add_task(function* test_mixedcontent() {
   yield new Promise(resolve => SpecialPowers.pushPrefEnv({
     "set": [["security.mixed_content.block_active_content", false]],
   }, resolve));
 
   // Load the page with the subframe in a new tab.
+  let testUrlPath = "://example.com" + TEST_URL_PATH;
   let tab = gBrowser.addTab("https" + testUrlPath + "insecure_test.html");
   let browser = tab.linkedBrowser;
   yield Promise.all([
     BrowserTestUtils.switchTab(gBrowser, tab),
     BrowserTestUtils.browserLoaded(browser),
     // Two events are triggered by pageshow and one by DOMFormHasPassword.
     waitForInsecureLoginFormsStateChange(browser, 3),
   ]);
--- a/browser/components/loop/content/css/panel.css
+++ b/browser/components/loop/content/css/panel.css
@@ -386,18 +386,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;
   -moz-margin-end: 1rem;
   vertical-align: middle;
 }
 
-.room-entry-context-item > img {
-  height: 16px;
+.room-entry-context-item > a > img {
   width: 16px;
 }
 
 .button-close {
   background-color: transparent;
   background-image: url(../shared/img/icons-10x10.svg#close);
   background-repeat: no-repeat;
   background-size: 8px 8px;
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -793,13 +793,18 @@ usercontext.work.label = Work
 usercontext.shopping.label = Shopping
 usercontext.banking.label = Banking
 
 muteTab.label = Mute Tab
 muteTab.accesskey = M
 unmuteTab.label = Unmute Tab
 unmuteTab.accesskey = M
 
+# LOCALIZATION NOTE (weakCryptoOverriding.message): %S is brandShortName
+weakCryptoOverriding.message = %S recommends that you don't enter your password, credit card and other personal information on this website.
+revokeOverride.label = Don't Trust This Website
+revokeOverride.accesskey = D
+
 # LOCALIZATION NOTE (tabgroups.deprecationwarning.description):
 # %S is brandShortName
 tabgroups.deprecationwarning.description         = Heads up! Tab Groups will be removed from %S soon.
 tabgroups.deprecationwarning.learnMore.label     = Learn More
 tabgroups.deprecationwarning.learnMore.accesskey = L
--- a/devtools/client/debugger/test/mochitest/head.js
+++ b/devtools/client/debugger/test/mochitest/head.js
@@ -1,47 +1,35 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
-var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
-
-var { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+// shared-head.js handles imports, constants, and utility functions
+Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js", this);
 
 // Disable logging for faster test runs. Set this pref to true if you want to
 // debug a test in your try runs. Both the debugger server and frontend will
 // be affected by this pref.
 var gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
 Services.prefs.setBoolPref("devtools.debugger.log", false);
 
-var { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
-var { Promise: promise } = Cu.import("resource://devtools/shared/deprecated-sync-thenables.js", {});
-var { gDevTools } = Cu.import("resource://devtools/client/framework/gDevTools.jsm", {});
-var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
-var DevToolsUtils = require("devtools/shared/DevToolsUtils");
 var { BrowserToolboxProcess } = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
 var { DebuggerServer } = require("devtools/server/main");
 var { DebuggerClient, ObjectClient } = require("devtools/shared/client/main");
 var { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {});
 var EventEmitter = require("devtools/shared/event-emitter");
 const { promiseInvoke } = require("devtools/shared/async-utils");
-var { TargetFactory } = require("devtools/client/framework/target");
 var { Toolbox } = require("devtools/client/framework/toolbox")
 
+// Override promise with deprecated-sync-thenables
+promise = Cu.import("resource://devtools/shared/deprecated-sync-thenables.js", {}).Promise;
+
 const EXAMPLE_URL = "http://example.com/browser/devtools/client/debugger/test/mochitest/";
 const FRAME_SCRIPT_URL = getRootDirectory(gTestPath) + "code_frame-script.js";
 
-DevToolsUtils.testing = true;
-SimpleTest.registerCleanupFunction(() => {
-  DevToolsUtils.testing = false;
-});
-
-// All tests are asynchronous.
-waitForExplicitFinish();
-
 registerCleanupFunction(function* () {
   info("finish() was called, cleaning up...");
   Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
 
   while (gBrowser && gBrowser.tabs && gBrowser.tabs.length > 1) {
     info("Destroying toolbox.");
     let target = TargetFactory.forTab(gBrowser.selectedTab);
     yield gDevTools.closeToolbox(target);
@@ -79,17 +67,19 @@ function addWindow(aUrl) {
 
 function getChromeWindow(aWindow) {
   return aWindow
     .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
     .QueryInterface(Ci.nsIDocShellTreeItem).rootTreeItem
     .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
 }
 
-function addTab(aUrl, aWindow) {
+// Override addTab/removeTab as defined by shared-head, since these have
+// an extra window parameter and add a frame script
+this.addTab = function addTab(aUrl, aWindow) {
   info("Adding tab: " + aUrl);
 
   let deferred = promise.defer();
   let targetWindow = aWindow || window;
   let targetBrowser = targetWindow.gBrowser;
 
   targetWindow.focus();
   let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl);
@@ -102,17 +92,17 @@ function addTab(aUrl, aWindow) {
     linkedBrowser.removeEventListener("load", onLoad, true);
     info("Tab added and finished loading: " + aUrl);
     deferred.resolve(tab);
   }, true);
 
   return deferred.promise;
 }
 
-function removeTab(aTab, aWindow) {
+this.removeTab = function removeTab(aTab, aWindow) {
   info("Removing tab.");
 
   let deferred = promise.defer();
   let targetWindow = aWindow || window;
   let targetBrowser = targetWindow.gBrowser;
   let tabContainer = targetBrowser.tabContainer;
 
   tabContainer.addEventListener("TabClose", function onClose(aEvent) {
@@ -1210,35 +1200,8 @@ function getSplitConsole(toolbox, win) {
   return new Promise(resolve => {
     toolbox.getPanelWhenReady("webconsole").then(() => {
       ok(toolbox.splitConsole, "Split console is shown.");
       let jsterm = toolbox.getPanel("webconsole").hud.jsterm;
       resolve(jsterm);
     });
   });
 }
-
-// This can be removed once debugger uses shared-head.js (bug 1181838)
-function synthesizeKeyFromKeyTag(key) {
-  is(key && key.tagName, "key", "Successfully retrieved the <key> node");
-
-  let modifiersAttr = key.getAttribute("modifiers");
-
-  let name = null;
-
-  if (key.getAttribute("keycode"))
-    name = key.getAttribute("keycode");
-  else if (key.getAttribute("key"))
-    name = key.getAttribute("key");
-
-  isnot(name, null, "Successfully retrieved keycode/key");
-
-  let modifiers = {
-    shiftKey: !!modifiersAttr.match("shift"),
-    ctrlKey: !!modifiersAttr.match("control"),
-    altKey: !!modifiersAttr.match("alt"),
-    metaKey: !!modifiersAttr.match("meta"),
-    accelKey: !!modifiersAttr.match("accel")
-  };
-
-  info("Synthesizing key " + name + " " + JSON.stringify(modifiers));
-  EventUtils.synthesizeKey(name, modifiers);
-}
--- a/devtools/client/framework/selection.js
+++ b/devtools/client/framework/selection.js
@@ -157,16 +157,22 @@ Selection.prototype = {
       return this.node.ownerDocument;
     }
     return null;
   },
 
   setNodeFront: function(value, reason="unknown") {
     this.reason = reason;
 
+    // If a singleTextChild text node is being set, then set it's parent instead.
+    let parentNode = value && value.parentNode();
+    if (value && parentNode && parentNode.singleTextChild === value) {
+      value = parentNode;
+    }
+
     // We used to return here if the node had not changed but we now need to
     // set the node even if it is already set otherwise it is not possible to
     // e.g. highlight the same node twice.
     let rawValue = null;
     if (value && value.isLocal_toBeDeprecated()) {
       rawValue = value.rawNode();
     }
     this.emit("before-new-node", rawValue, reason);
--- a/devtools/client/framework/test/shared-head.js
+++ b/devtools/client/framework/test/shared-head.js
@@ -15,17 +15,17 @@ function scopedCuImport(path) {
 const {Services} = scopedCuImport("resource://gre/modules/Services.jsm");
 const {gDevTools} = scopedCuImport("resource://devtools/client/framework/gDevTools.jsm");
 const {console} = scopedCuImport("resource://gre/modules/Console.jsm");
 const {ScratchpadManager} = scopedCuImport("resource://devtools/client/scratchpad/scratchpad-manager.jsm");
 const {require} = scopedCuImport("resource://devtools/shared/Loader.jsm");
 
 const {TargetFactory} = require("devtools/client/framework/target");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
-const promise = require("promise");
+let promise = require("promise");
 
 const TEST_DIR = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
 const CHROME_URL_ROOT = TEST_DIR + "/";
 const URL_ROOT = CHROME_URL_ROOT.replace("chrome://mochitests/content/", "http://example.com/");
 
 // All test are asynchronous
 waitForExplicitFinish();
 
--- a/devtools/client/inspector/inspector-panel.js
+++ b/devtools/client/inspector/inspector-panel.js
@@ -11,17 +11,17 @@ Cu.import("resource://gre/modules/Servic
 var promise = require("promise");
 var EventEmitter = require("devtools/shared/event-emitter");
 var clipboard = require("sdk/clipboard");
 var {HostType} = require("devtools/client/framework/toolbox").Toolbox;
 
 loader.lazyGetter(this, "MarkupView", () => require("devtools/client/markupview/markup-view").MarkupView);
 loader.lazyGetter(this, "HTMLBreadcrumbs", () => require("devtools/client/inspector/breadcrumbs").HTMLBreadcrumbs);
 loader.lazyGetter(this, "ToolSidebar", () => require("devtools/client/framework/sidebar").ToolSidebar);
-loader.lazyGetter(this, "SelectorSearch", () => require("devtools/client/inspector/selector-search").SelectorSearch);
+loader.lazyGetter(this, "InspectorSearch", () => require("devtools/client/inspector/inspector-search").InspectorSearch);
 
 loader.lazyGetter(this, "strings", () => {
   return Services.strings.createBundle("chrome://devtools/locale/inspector.properties");
 });
 loader.lazyGetter(this, "toolboxStrings", () => {
   return Services.strings.createBundle("chrome://devtools/locale/toolbox.properties");
 });
 loader.lazyGetter(this, "clipboardHelper", () => {
@@ -73,17 +73,30 @@ const LAYOUT_CHANGE_TIMER = 250;
 function InspectorPanel(iframeWindow, toolbox) {
   this._toolbox = toolbox;
   this._target = toolbox._target;
   this.panelDoc = iframeWindow.document;
   this.panelWin = iframeWindow;
   this.panelWin.inspector = this;
 
   this.nodeMenuTriggerInfo = null;
+
   this._onBeforeNavigate = this._onBeforeNavigate.bind(this);
+  this.onNewRoot = this.onNewRoot.bind(this);
+  this._setupNodeMenu = this._setupNodeMenu.bind(this);
+  this._resetNodeMenu = this._resetNodeMenu.bind(this);
+  this._updateSearchResultsLabel = this._updateSearchResultsLabel.bind(this);
+  this.onNewSelection = this.onNewSelection.bind(this);
+  this.onBeforeNewSelection = this.onBeforeNewSelection.bind(this);
+  this.onDetached = this.onDetached.bind(this);
+  this.onToolboxHostChanged = this.onToolboxHostChanged.bind(this);
+  this.scheduleLayoutChange = this.scheduleLayoutChange.bind(this);
+  this.onPaneToggleButtonClicked = this.onPaneToggleButtonClicked.bind(this);
+  this._onMarkupFrameLoad = this._onMarkupFrameLoad.bind(this);
+
   this._target.on("will-navigate", this._onBeforeNavigate);
 
   EventEmitter.decorate(this);
 }
 
 exports.InspectorPanel = InspectorPanel;
 
 InspectorPanel.prototype = {
@@ -134,41 +147,33 @@ InspectorPanel.prototype = {
 
   get canPasteInnerOrAdjacentHTML() {
     return this._target.client.traits.pasteHTML;
   },
 
   _deferredOpen: function(defaultSelection) {
     let deferred = promise.defer();
 
-    this.onNewRoot = this.onNewRoot.bind(this);
     this.walker.on("new-root", this.onNewRoot);
 
     this.nodemenu = this.panelDoc.getElementById("inspector-node-popup");
     this.lastNodemenuItem = this.nodemenu.lastChild;
-    this._setupNodeMenu = this._setupNodeMenu.bind(this);
-    this._resetNodeMenu = this._resetNodeMenu.bind(this);
     this.nodemenu.addEventListener("popupshowing", this._setupNodeMenu, true);
     this.nodemenu.addEventListener("popuphiding", this._resetNodeMenu, true);
 
-    this.onNewSelection = this.onNewSelection.bind(this);
     this.selection.on("new-node-front", this.onNewSelection);
-    this.onBeforeNewSelection = this.onBeforeNewSelection.bind(this);
     this.selection.on("before-new-node-front", this.onBeforeNewSelection);
-    this.onDetached = this.onDetached.bind(this);
     this.selection.on("detached-front", this.onDetached);
 
     this.breadcrumbs = new HTMLBreadcrumbs(this);
 
-    this.onToolboxHostChanged = this.onToolboxHostChanged.bind(this);
     this._toolbox.on("host-changed", this.onToolboxHostChanged);
 
     if (this.target.isLocalTab) {
       this.browser = this.target.tab.linkedBrowser;
-      this.scheduleLayoutChange = this.scheduleLayoutChange.bind(this);
       this.browser.addEventListener("resize", this.scheduleLayoutChange, true);
 
       // Show a warning when the debugger is paused.
       // We show the warning only when the inspector
       // is selected.
       this.updateDebuggerPausedWarning = () => {
         let notificationBox = this._toolbox.getNotificationBox();
         let notification = notificationBox.getNotificationWithValue("inspector-script-paused");
@@ -304,23 +309,41 @@ InspectorPanel.prototype = {
   markDirty: function() {
     this.isDirty = true;
   },
 
   /**
    * Hooks the searchbar to show result and auto completion suggestions.
    */
   setupSearchBox: function() {
-    // Initiate the selectors search object.
-    if (this.searchSuggestions) {
-      this.searchSuggestions.destroy();
-      this.searchSuggestions = null;
+    this.searchBox = this.panelDoc.getElementById("inspector-searchbox");
+    this.searchResultsLabel = this.panelDoc.getElementById("inspector-searchlabel");
+
+    this.search = new InspectorSearch(this, this.searchBox);
+    this.search.on("search-cleared", this._updateSearchResultsLabel);
+    this.search.on("search-result", this._updateSearchResultsLabel);
+  },
+
+  get searchSuggestions() {
+    return this.search.autocompleter;
+  },
+
+  _updateSearchResultsLabel: function(event, result) {
+    let str = "";
+    if (event !== "search-cleared") {
+      if (result) {
+        str = strings.formatStringFromName(
+          "inspector.searchResultsCount2",
+          [result.resultsIndex + 1, result.resultsLength], 2);
+      } else {
+        str = strings.GetStringFromName("inspector.searchResultsNone");
+      }
     }
-    this.searchBox = this.panelDoc.getElementById("inspector-searchbox");
-    this.searchSuggestions = new SelectorSearch(this, this.searchBox);
+
+    this.searchResultsLabel.textContent = str;
   },
 
   /**
    * Build the sidebar.
    */
   setupSidebar: function() {
     let tabbox = this.panelDoc.querySelector("#inspector-sidebar");
     this.sidebar = new ToolSidebar(tabbox, this, "inspector", {
@@ -364,17 +387,16 @@ InspectorPanel.prototype = {
     this.setupSidebarToggle();
   },
 
   /**
    * Add the expand/collapse behavior for the sidebar panel.
    */
   setupSidebarToggle: function() {
     this._paneToggleButton = this.panelDoc.getElementById("inspector-pane-toggle");
-    this.onPaneToggleButtonClicked = this.onPaneToggleButtonClicked.bind(this);
     this._paneToggleButton.addEventListener("mousedown",
       this.onPaneToggleButtonClicked);
     this.updatePaneToggleButton();
   },
 
   /**
    * Reset the inspector on new root mutation.
    */
@@ -394,17 +416,16 @@ InspectorPanel.prototype = {
       this.selection.setNodeFront(defaultNode, "navigateaway");
 
       this._initMarkup();
       this.once("markuploaded", () => {
         if (!this.markup) {
           return;
         }
         this.markup.expandNode(this.selection.nodeFront);
-        this.setupSearchBox();
         this.emit("new-root");
       });
     };
     this._pendingSelection = onNodeSelected;
     this._getDefaultNodeForSelection().then(onNodeSelected, console.error);
   },
 
   _selectionCssSelector: null,
@@ -585,32 +606,32 @@ InspectorPanel.prototype = {
     this.sidebar = null;
 
     this.nodemenu.removeEventListener("popupshowing", this._setupNodeMenu, true);
     this.nodemenu.removeEventListener("popuphiding", this._resetNodeMenu, true);
     this.breadcrumbs.destroy();
     this._paneToggleButton.removeEventListener("mousedown",
       this.onPaneToggleButtonClicked);
     this._paneToggleButton = null;
-    this.searchSuggestions.destroy();
-    this.searchBox = null;
     this.selection.off("new-node-front", this.onNewSelection);
     this.selection.off("before-new-node", this.onBeforeNewSelection);
     this.selection.off("before-new-node-front", this.onBeforeNewSelection);
     this.selection.off("detached-front", this.onDetached);
     let markupDestroyer = this._destroyMarkup();
     this.panelWin.inspector = null;
     this.target = null;
     this.panelDoc = null;
     this.panelWin = null;
     this.breadcrumbs = null;
-    this.searchSuggestions = null;
     this.lastNodemenuItem = null;
     this.nodemenu = null;
     this._toolbox = null;
+    this.search.destroy();
+    this.search = null;
+    this.searchBox = null;
 
     this._panelDestroyer = promise.all([
       sidebarDestroyer,
       markupDestroyer
     ]);
 
     return this._panelDestroyer;
   },
@@ -909,45 +930,42 @@ InspectorPanel.prototype = {
 
     // create tool iframe
     this._markupFrame = doc.createElement("iframe");
     this._markupFrame.setAttribute("flex", "1");
     this._markupFrame.setAttribute("tooltip", "aHTMLTooltip");
     this._markupFrame.setAttribute("context", "inspector-node-popup");
 
     // This is needed to enable tooltips inside the iframe document.
-    this._boundMarkupFrameLoad = this._onMarkupFrameLoad.bind(this);
-    this._markupFrame.addEventListener("load", this._boundMarkupFrameLoad, true);
+    this._markupFrame.addEventListener("load", this._onMarkupFrameLoad, true);
 
     this._markupBox.setAttribute("collapsed", true);
     this._markupBox.appendChild(this._markupFrame);
     this._markupFrame.setAttribute("src", "chrome://devtools/content/markupview/markup-view.xhtml");
     this._markupFrame.setAttribute("aria-label", strings.GetStringFromName("inspector.panelLabel.markupView"));
   },
 
   _onMarkupFrameLoad: function() {
-    this._markupFrame.removeEventListener("load", this._boundMarkupFrameLoad, true);
-    delete this._boundMarkupFrameLoad;
+    this._markupFrame.removeEventListener("load", this._onMarkupFrameLoad, true);
 
     this._markupFrame.contentWindow.focus();
 
     this._markupBox.removeAttribute("collapsed");
 
     let controllerWindow = this._toolbox.doc.defaultView;
     this.markup = new MarkupView(this, this._markupFrame, controllerWindow);
 
     this.emit("markuploaded");
   },
 
   _destroyMarkup: function() {
     let destroyPromise;
 
-    if (this._boundMarkupFrameLoad) {
-      this._markupFrame.removeEventListener("load", this._boundMarkupFrameLoad, true);
-      this._boundMarkupFrameLoad = null;
+    if (this._markupFrame) {
+      this._markupFrame.removeEventListener("load", this._onMarkupFrameLoad, true);
     }
 
     if (this.markup) {
       destroyPromise = this.markup.destroy();
       this.markup = null;
     } else {
       destroyPromise = promise.resolve();
     }
rename from devtools/client/inspector/selector-search.js
rename to devtools/client/inspector/inspector-search.js
--- a/devtools/client/inspector/selector-search.js
+++ b/devtools/client/inspector/inspector-search.js
@@ -1,54 +1,146 @@
 /* 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 { Cu } = require("chrome");
+const {Cu, Ci} = require("chrome");
 
 const promise = require("promise");
 loader.lazyGetter(this, "EventEmitter", () => require("devtools/shared/event-emitter"));
 loader.lazyGetter(this, "AutocompletePopup", () => require("devtools/client/shared/autocomplete-popup").AutocompletePopup);
 
 // Maximum number of selector suggestions shown in the panel.
 const MAX_SUGGESTIONS = 15;
 
+
+/**
+ * Converts any input field into a document search box.
+ *
+ * @param {InspectorPanel} inspector The InspectorPanel whose `walker` attribute
+ * should be used for document traversal.
+ * @param {DOMNode} input The input element to which the panel will be attached
+ * and from where search input will be taken.
+ *
+ * Emits the following events:
+ * - search-cleared: when the search box is emptied
+ * - search-result: when a search is made and a result is selected
+ */
+function InspectorSearch(inspector, input) {
+  this.inspector = inspector;
+  this.searchBox = input;
+  this._lastSearched = null;
+
+  this._onKeyDown = this._onKeyDown.bind(this);
+  this._onCommand = this._onCommand.bind(this);
+  this.searchBox.addEventListener("keydown", this._onKeyDown, true);
+  this.searchBox.addEventListener("command", this._onCommand, true);
+
+  // For testing, we need to be able to wait for the most recent node request
+  // to finish.  Tests can watch this promise for that.
+  this._lastQuery = promise.resolve(null);
+
+  this.autocompleter = new SelectorAutocompleter(inspector, input);
+  EventEmitter.decorate(this);
+}
+
+exports.InspectorSearch = InspectorSearch;
+
+InspectorSearch.prototype = {
+  get walker() {
+    return this.inspector.walker;
+  },
+
+  destroy: function() {
+    this.searchBox.removeEventListener("keydown", this._onKeyDown, true);
+    this.searchBox.removeEventListener("command", this._onCommand, true);
+    this.searchBox = null;
+    this.autocompleter.destroy();
+  },
+
+  _onSearch: function(reverse = false) {
+    this.doFullTextSearch(this.searchBox.value, reverse)
+        .catch(e => console.error(e));
+  },
+
+  doFullTextSearch: Task.async(function*(query, reverse) {
+    let lastSearched = this._lastSearched;
+    this._lastSearched = query;
+
+    if (query.length === 0) {
+      this.searchBox.classList.remove("devtools-no-search-result");
+      if (!lastSearched || lastSearched.length > 0) {
+        this.emit("search-cleared");
+      }
+      return;
+    }
+
+    let res = yield this.walker.search(query, { reverse });
+
+    // Value has changed since we started this request, we're done.
+    if (query != this.searchBox.value) {
+      return;
+    }
+
+    if (res) {
+      this.inspector.selection.setNodeFront(res.node, "inspectorsearch");
+      this.searchBox.classList.remove("devtools-no-search-result");
+
+      res.query = query;
+      this.emit("search-result", res);
+    } else {
+      this.searchBox.classList.add("devtools-no-search-result");
+      this.emit("search-result");
+    }
+  }),
+
+  _onCommand: function() {
+    if (this.searchBox.value.length === 0) {
+      this._onSearch();
+    }
+  },
+
+  _onKeyDown: function(event) {
+    if (this.searchBox.value.length === 0) {
+      this.searchBox.removeAttribute("filled");
+    } else {
+      this.searchBox.setAttribute("filled", true);
+    }
+    if (event.keyCode === event.DOM_VK_RETURN) {
+      this._onSearch();
+    } if (event.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_G && event.metaKey) {
+      this._onSearch(event.shiftKey);
+      event.preventDefault();
+    }
+  }
+};
+
 /**
  * Converts any input box on a page to a CSS selector search and suggestion box.
  *
  * Emits 'processing-done' event when it is done processing the current
  * keypress, search request or selection from the list, whether that led to a
  * search or not.
  *
  * @constructor
- * @param InspectorPanel aInspector
+ * @param InspectorPanel inspector
  *        The InspectorPanel whose `walker` attribute should be used for
  *        document traversal.
- * @param nsiInputElement aInputNode
+ * @param nsiInputElement inputNode
  *        The input element to which the panel will be attached and from where
  *        search input will be taken.
  */
-function SelectorSearch(aInspector, aInputNode) {
-  this.inspector = aInspector;
-  this.searchBox = aInputNode;
+function SelectorAutocompleter(inspector, inputNode) {
+  this.inspector = inspector;
+  this.searchBox = inputNode;
   this.panelDoc = this.searchBox.ownerDocument;
 
-  // initialize variables.
-  this._lastSearched = null;
-  this._lastValidSearch = "";
-  this._lastToLastValidSearch = null;
-  this._searchResults = null;
-  this._searchSuggestions = {};
-  this._searchIndex = 0;
-
-  // bind!
-  this._showPopup = this._showPopup.bind(this);
-  this._onHTMLSearch = this._onHTMLSearch.bind(this);
+  this.showSuggestions = this.showSuggestions.bind(this);
   this._onSearchKeypress = this._onSearchKeypress.bind(this);
   this._onListBoxKeypress = this._onListBoxKeypress.bind(this);
   this._onMarkupMutation = this._onMarkupMutation.bind(this);
 
   // Options for the AutocompletePopup.
   let options = {
     panelId: "inspector-searchbox-panel",
     listBoxId: "searchbox-panel-listbox",
@@ -56,30 +148,29 @@ function SelectorSearch(aInspector, aInp
     position: "before_start",
     direction: "ltr",
     theme: "auto",
     onClick: this._onListBoxKeypress,
     onKeypress: this._onListBoxKeypress
   };
   this.searchPopup = new AutocompletePopup(this.panelDoc, options);
 
-  // event listeners.
-  this.searchBox.addEventListener("command", this._onHTMLSearch, true);
+  this.searchBox.addEventListener("input", this.showSuggestions, true);
   this.searchBox.addEventListener("keypress", this._onSearchKeypress, true);
   this.inspector.on("markupmutation", this._onMarkupMutation);
 
   // For testing, we need to be able to wait for the most recent node request
   // to finish.  Tests can watch this promise for that.
   this._lastQuery = promise.resolve(null);
   EventEmitter.decorate(this);
 }
 
-exports.SelectorSearch = SelectorSearch;
+exports.SelectorAutocompleter = SelectorAutocompleter;
 
-SelectorSearch.prototype = {
+SelectorAutocompleter.prototype = {
   get walker() {
     return this.inspector.walker;
   },
 
   // The possible states of the query.
   States: {
     CLASS: "class",
     ID: "id",
@@ -164,268 +255,123 @@ SelectorSearch.prototype = {
     }
     return this._state;
   },
 
   /**
    * Removes event listeners and cleans up references.
    */
   destroy: function() {
-    // event listeners.
-    this.searchBox.removeEventListener("command", this._onHTMLSearch, true);
+    this.searchBox.removeEventListener("input", this.showSuggestions, true);
     this.searchBox.removeEventListener("keypress", this._onSearchKeypress, true);
     this.inspector.off("markupmutation", this._onMarkupMutation);
     this.searchPopup.destroy();
     this.searchPopup = null;
     this.searchBox = null;
     this.panelDoc = null;
-    this._searchResults = null;
-    this._searchSuggestions = null;
-  },
-
-  _selectResult: function(index) {
-    return this._searchResults.item(index).then(node => {
-      this.inspector.selection.setNodeFront(node, "selectorsearch");
-    });
-  },
-
-  _queryNodes: Task.async(function*(query) {
-    if (typeof this.hasMultiFrameSearch === "undefined") {
-      let target = this.inspector.toolbox.target;
-      this.hasMultiFrameSearch = yield target.actorHasMethod("domwalker",
-        "multiFrameQuerySelectorAll");
-    }
-
-    if (this.hasMultiFrameSearch) {
-      return yield this.walker.multiFrameQuerySelectorAll(query);
-    } else {
-      return yield this.walker.querySelectorAll(this.walker.rootNode, query);
-    }
-  }),
-
-  /**
-   * The command callback for the input box. This function is automatically
-   * invoked as the user is typing if the input box type is search.
-   */
-  _onHTMLSearch: function() {
-    let query = this.searchBox.value;
-    if (query == this._lastSearched) {
-      this.emit("processing-done");
-      return;
-    }
-    this._lastSearched = query;
-    this._searchResults = [];
-    this._searchIndex = 0;
-
-    if (query.length == 0) {
-      this._lastValidSearch = "";
-      this.searchBox.removeAttribute("filled");
-      this.searchBox.classList.remove("devtools-no-search-result");
-      if (this.searchPopup.isOpen) {
-        this.searchPopup.hidePopup();
-      }
-      this.emit("processing-done");
-      return;
-    }
-
-    this.searchBox.setAttribute("filled", true);
-    let queryList = null;
-
-    this._lastQuery = this._queryNodes(query).then(list => {
-      return list;
-    }, (err) => {
-      // Failures are ok here, just use a null item list;
-      return null;
-    }).then(queryList => {
-      // Value has changed since we started this request, we're done.
-      if (query != this.searchBox.value) {
-        if (queryList) {
-          queryList.release();
-        }
-        return promise.reject(null);
-      }
-
-      this._searchResults = queryList || [];
-      if (this._searchResults && this._searchResults.length > 0) {
-        this._lastValidSearch = query;
-        // Even though the selector matched atleast one node, there is still
-        // possibility of suggestions.
-        if (query.match(/[\s>+]$/)) {
-          // If the query has a space or '>' at the end, create a selector to match
-          // the children of the selector inside the search box by adding a '*'.
-          this._lastValidSearch += "*";
-        }
-        else if (query.match(/[\s>+][\.#a-zA-Z][\.#>\s+]*$/)) {
-          // If the query is a partial descendant selector which does not matches
-          // any node, remove the last incomplete part and add a '*' to match
-          // everything. For ex, convert 'foo > b' to 'foo > *' .
-          let lastPart = query.match(/[\s>+][\.#a-zA-Z][^>\s+]*$/)[0];
-          this._lastValidSearch = query.slice(0, -1 * lastPart.length + 1) + "*";
-        }
-
-        if (!query.slice(-1).match(/[\.#\s>+]/)) {
-          // Hide the popup if we have some matching nodes and the query is not
-          // ending with [.# >] which means that the selector is not at the
-          // beginning of a new class, tag or id.
-          if (this.searchPopup.isOpen) {
-            this.searchPopup.hidePopup();
-          }
-          this.searchBox.classList.remove("devtools-no-search-result");
-
-          return this._selectResult(0);
-        }
-        return this._selectResult(0).then(() => {
-          this.searchBox.classList.remove("devtools-no-search-result");
-        }).then(() => this.showSuggestions());
-      }
-      if (query.match(/[\s>+]$/)) {
-        this._lastValidSearch = query + "*";
-      }
-      else if (query.match(/[\s>+][\.#a-zA-Z][\.#>\s+]*$/)) {
-        let lastPart = query.match(/[\s+>][\.#a-zA-Z][^>\s+]*$/)[0];
-        this._lastValidSearch = query.slice(0, -1 * lastPart.length + 1) + "*";
-      }
-      this.searchBox.classList.add("devtools-no-search-result");
-      return this.showSuggestions();
-    }).then(() => this.emit("processing-done"), Cu.reportError);
   },
 
   /**
    * Handles keypresses inside the input box.
    */
-  _onSearchKeypress: function(aEvent) {
+  _onSearchKeypress: function(event) {
     let query = this.searchBox.value;
-    switch(aEvent.keyCode) {
-      case aEvent.DOM_VK_RETURN:
-        if (query == this._lastSearched && this._searchResults) {
-          this._searchIndex = (this._searchIndex + 1) % this._searchResults.length;
-        }
-        else {
-          this._onHTMLSearch();
-          return;
+    switch(event.keyCode) {
+      case event.DOM_VK_RETURN:
+      case event.DOM_VK_TAB:
+        if (this.searchPopup.isOpen &&
+            this.searchPopup.getItemAtIndex(this.searchPopup.itemCount - 1)
+                .preLabel == query) {
+          this.searchPopup.selectedIndex = this.searchPopup.itemCount - 1;
+          this.searchBox.value = this.searchPopup.selectedItem.label;
+          this.hidePopup();
         }
         break;
 
-      case aEvent.DOM_VK_UP:
+      case event.DOM_VK_UP:
         if (this.searchPopup.isOpen && this.searchPopup.itemCount > 0) {
           this.searchPopup.focus();
           if (this.searchPopup.selectedIndex == this.searchPopup.itemCount - 1) {
             this.searchPopup.selectedIndex =
               Math.max(0, this.searchPopup.itemCount - 2);
           }
           else {
             this.searchPopup.selectedIndex = this.searchPopup.itemCount - 1;
           }
           this.searchBox.value = this.searchPopup.selectedItem.label;
         }
-        else if (--this._searchIndex < 0) {
-          this._searchIndex = this._searchResults.length - 1;
-        }
         break;
 
-      case aEvent.DOM_VK_DOWN:
+      case event.DOM_VK_DOWN:
         if (this.searchPopup.isOpen && this.searchPopup.itemCount > 0) {
           this.searchPopup.focus();
           this.searchPopup.selectedIndex = 0;
           this.searchBox.value = this.searchPopup.selectedItem.label;
         }
-        this._searchIndex = (this._searchIndex + 1) % this._searchResults.length;
         break;
 
-      case aEvent.DOM_VK_TAB:
-        if (this.searchPopup.isOpen &&
-            this.searchPopup.getItemAtIndex(this.searchPopup.itemCount - 1)
-                .preLabel == query) {
-          this.searchPopup.selectedIndex = this.searchPopup.itemCount - 1;
-          this.searchBox.value = this.searchPopup.selectedItem.label;
-          this._onHTMLSearch();
-        }
-        break;
-
-      case aEvent.DOM_VK_BACK_SPACE:
-      case aEvent.DOM_VK_DELETE:
-        // need to throw away the lastValidSearch.
-        this._lastToLastValidSearch = null;
-        // This gets the most complete selector from the query. For ex.
-        // '.foo.ba' returns '.foo' , '#foo > .bar.baz' returns '#foo > .bar'
-        // '.foo +bar' returns '.foo +' and likewise.
-        this._lastValidSearch = (query.match(/(.*)[\.#][^\.# ]{0,}$/) ||
-                                 query.match(/(.*[\s>+])[a-zA-Z][^\.# ]{0,}$/) ||
-                                 ["",""])[1];
-        return;
-
       default:
         return;
     }
 
-    aEvent.preventDefault();
-    aEvent.stopPropagation();
-    if (this._searchResults && this._searchResults.length > 0) {
-      this._lastQuery = this._selectResult(this._searchIndex).then(() => this.emit("processing-done"));
-    }
-    else {
-      this.emit("processing-done");
-    }
+    event.preventDefault();
+    event.stopPropagation();
+    this.emit("processing-done");
   },
 
   /**
    * Handles keypress and mouse click on the suggestions richlistbox.
    */
-  _onListBoxKeypress: function(aEvent) {
-    switch(aEvent.keyCode || aEvent.button) {
-      case aEvent.DOM_VK_RETURN:
-      case aEvent.DOM_VK_TAB:
+  _onListBoxKeypress: function(event) {
+    switch(event.keyCode || event.button) {
+      case event.DOM_VK_RETURN:
+      case event.DOM_VK_TAB:
       case 0: // left mouse button
-        aEvent.stopPropagation();
-        aEvent.preventDefault();
+        event.stopPropagation();
+        event.preventDefault();
         this.searchBox.value = this.searchPopup.selectedItem.label;
         this.searchBox.focus();
-        this._onHTMLSearch();
+        this.hidePopup();
         break;
 
-      case aEvent.DOM_VK_UP:
+      case event.DOM_VK_UP:
         if (this.searchPopup.selectedIndex == 0) {
           this.searchPopup.selectedIndex = -1;
-          aEvent.stopPropagation();
-          aEvent.preventDefault();
+          event.stopPropagation();
+          event.preventDefault();
           this.searchBox.focus();
         }
         else {
           let index = this.searchPopup.selectedIndex;
           this.searchBox.value = this.searchPopup.getItemAtIndex(index - 1).label;
         }
         break;
 
-      case aEvent.DOM_VK_DOWN:
+      case event.DOM_VK_DOWN:
         if (this.searchPopup.selectedIndex == this.searchPopup.itemCount - 1) {
           this.searchPopup.selectedIndex = -1;
-          aEvent.stopPropagation();
-          aEvent.preventDefault();
+          event.stopPropagation();
+          event.preventDefault();
           this.searchBox.focus();
         }
         else {
           let index = this.searchPopup.selectedIndex;
           this.searchBox.value = this.searchPopup.getItemAtIndex(index + 1).label;
         }
         break;
 
-      case aEvent.DOM_VK_BACK_SPACE:
-        aEvent.stopPropagation();
-        aEvent.preventDefault();
+      case event.DOM_VK_BACK_SPACE:
+        event.stopPropagation();
+        event.preventDefault();
         this.searchBox.focus();
         if (this.searchBox.selectionStart > 0) {
           this.searchBox.value =
             this.searchBox.value.substring(0, this.searchBox.selectionStart - 1);
         }
-        this._lastToLastValidSearch = null;
-        let query = this.searchBox.value;
-        this._lastValidSearch = (query.match(/(.*)[\.#][^\.# ]{0,}$/) ||
-                                 query.match(/(.*[\s>+])[a-zA-Z][^\.# ]{0,}$/) ||
-                                 ["",""])[1];
-        this._onHTMLSearch();
+        this.hidePopup();
         break;
     }
     this.emit("processing-done");
   },
 
   /**
    * Reset previous search results on markup-mutations to make sure we search
    * again after nodes have been added/removed/changed.
@@ -433,22 +379,22 @@ SelectorSearch.prototype = {
   _onMarkupMutation: function() {
     this._searchResults = null;
     this._lastSearched = null;
   },
 
   /**
    * Populates the suggestions list and show the suggestion popup.
    */
-  _showPopup: function(aList, aFirstPart, aState) {
+  _showPopup: function(list, firstPart, aState) {
     let total = 0;
     let query = this.searchBox.value;
     let items = [];
 
-    for (let [value, count, state] of aList) {
+    for (let [value, /*count*/, state] of list) {
       // for cases like 'div ' or 'div >' or 'div+'
       if (query.match(/[\s>+]$/)) {
         value = query + value;
       }
       // for cases like 'div #a' or 'div .a' or 'div > d' and likewise
       else if (query.match(/[\s>+][\.#a-zA-Z][^\s>+\.#]*$/)) {
         let lastPart = query.match(/[\s>+][\.#a-zA-Z][^>\s+\.#]*$/)[0];
         value = query.slice(0, -1 * lastPart.length + 1) + value;
@@ -456,18 +402,17 @@ SelectorSearch.prototype = {
       // for cases like 'div.class' or '#foo.bar' and likewise
       else if (query.match(/[a-zA-Z][#\.][^#\.\s+>]*$/)) {
         let lastPart = query.match(/[a-zA-Z][#\.][^#\.\s>+]*$/)[0];
         value = query.slice(0, -1 * lastPart.length + 1) + value;
       }
 
       let item = {
         preLabel: query,
-        label: value,
-        count: count
+        label: value
       };
 
       // In case of tagNames, change the case to small
       if (value.match(/.*[\.#][^\.#]{0,}$/) == null) {
         item.label = value.toLowerCase();
       }
 
       // In case the query's state is tag and the item's state is id or class
@@ -484,63 +429,81 @@ SelectorSearch.prototype = {
         break;
       }
     }
     if (total > 0) {
       this.searchPopup.setItems(items);
       this.searchPopup.openPopup(this.searchBox);
     }
     else {
+      this.hidePopup();
+    }
+  },
+
+
+  /**
+   * Hide the suggestion popup if necessary.
+   */
+  hidePopup: function() {
+    if (this.searchPopup.isOpen) {
       this.searchPopup.hidePopup();
     }
   },
 
   /**
    * Suggests classes,ids and tags based on the user input as user types in the
    * searchbox.
    */
   showSuggestions: function() {
     let query = this.searchBox.value;
     let state = this.state;
     let firstPart = "";
 
-    if (state == this.States.TAG) {
+    if (state === this.States.TAG) {
       // gets the tag that is being completed. For ex. 'div.foo > s' returns 's',
       // 'di' returns 'di' and likewise.
       firstPart = (query.match(/[\s>+]?([a-zA-Z]*)$/) || ["", query])[1];
       query = query.slice(0, query.length - firstPart.length);
     }
-    else if (state == this.States.CLASS) {
+    else if (state === this.States.CLASS) {
       // gets the class that is being completed. For ex. '.foo.b' returns 'b'
       firstPart = query.match(/\.([^\.]*)$/)[1];
       query = query.slice(0, query.length - firstPart.length - 1);
     }
-    else if (state == this.States.ID) {
+    else if (state === this.States.ID) {
       // gets the id that is being completed. For ex. '.foo#b' returns 'b'
       firstPart = query.match(/#([^#]*)$/)[1];
       query = query.slice(0, query.length - firstPart.length - 1);
     }
     // TODO: implement some caching so that over the wire request is not made
     // everytime.
     if (/[\s+>~]$/.test(query)) {
       query += "*";
     }
 
-    this._currentSuggesting = query;
-    return this.walker.getSuggestionsForQuery(query, firstPart, state).then(result => {
-      if (this._currentSuggesting != result.query) {
+    this._lastQuery = this.walker.getSuggestionsForQuery(query, firstPart, state).then(result => {
+      this.emit("processing-done");
+      if (result.query !== query) {
         // This means that this response is for a previous request and the user
         // as since typed something extra leading to a new request.
         return;
       }
-      this._lastToLastValidSearch = this._lastValidSearch;
 
-      if (state == this.States.CLASS) {
+      if (state === this.States.CLASS) {
         firstPart = "." + firstPart;
-      }
-      else if (state == this.States.ID) {
+      } else if (state === this.States.ID) {
         firstPart = "#" + firstPart;
       }
 
+      // If there is a single tag match and it's what the user typed, then
+      // don't need to show a popup.
+      if (result.suggestions.length === 1 &&
+          result.suggestions[0][0] === firstPart) {
+        result.suggestions = [];
+      }
+
+
       this._showPopup(result.suggestions, firstPart, state);
     });
+
+    return this._lastQuery;
   }
 };
--- a/devtools/client/inspector/inspector.xul
+++ b/devtools/client/inspector/inspector.xul
@@ -149,21 +149,22 @@
     <vbox flex="1" class="devtools-main-content">
       <toolbar id="inspector-toolbar"
         class="devtools-toolbar"
         nowindowdrag="true">
         <arrowscrollbox id="inspector-breadcrumbs"
           class="breadcrumbs-widget-container"
           flex="1" orient="horizontal"
           clicktoscroll="true"/>
+        <box id="inspector-searchlabel" />
         <textbox id="inspector-searchbox"
           type="search"
           timeout="50"
           class="devtools-searchinput"
-          placeholder="&inspectorSearchHTML.label2;"/>
+          placeholder="&inspectorSearchHTML.label3;"/>
         <toolbarbutton id="inspector-pane-toggle"
           class="devtools-toolbarbutton"
           tabindex="0" />
       </toolbar>
       <vbox flex="1" id="markup-box">
       </vbox>
     </vbox>
     <splitter class="devtools-side-splitter"/>
--- a/devtools/client/inspector/moz.build
+++ b/devtools/client/inspector/moz.build
@@ -1,12 +1,12 @@
 # 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/.
 
 DevToolsModules(
     'breadcrumbs.js',
     'inspector-commands.js',
     'inspector-panel.js',
-    'selector-search.js'
+    'inspector-search.js'
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
--- a/devtools/client/inspector/test/browser_inspector_search-01.js
+++ b/devtools/client/inspector/test/browser_inspector_search-01.js
@@ -3,76 +3,85 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 // Test that searching for nodes in the search field actually selects those
 // nodes.
 
 const TEST_URL = TEST_URL_ROOT + "doc_inspector_search.html";
 
-// Indexes of the keys in the KEY_STATES array that should listen to "keypress"
-// event instead of "command". These are keys that don't change the content of
-// the search field and thus don't trigger command event.
-const LISTEN_KEYPRESS = [3,4,8,18,19,20,21,22];
-
 // The various states of the inspector: [key, id, isValid]
 // [
 //  what key to press,
 //  what id should be selected after the keypress,
 //  is the searched text valid selector
 // ]
 const KEY_STATES = [
-  ["d", "b1", false],
-  ["i", "b1", false],
-  ["v", "d1", true],
-  ["VK_DOWN", "d2", true], // keypress
-  ["VK_RETURN", "d1", true], //keypress
-  [".", "d1", false],
-  ["c", "d1", false],
-  ["1", "d2", true],
-  ["VK_DOWN", "d2", true], // keypress
-  ["VK_BACK_SPACE", "d2", false],
-  ["VK_BACK_SPACE", "d2", false],
-  ["VK_BACK_SPACE", "d1", true],
-  ["VK_BACK_SPACE", "d1", false],
-  ["VK_BACK_SPACE", "d1", false],
-  ["VK_BACK_SPACE", "d1", true],
-  [".", "d1", false],
-  ["c", "d1", false],
-  ["1", "d2", true],
-  ["VK_DOWN", "s2", true], // keypress
-  ["VK_DOWN", "p1", true], // kepress
-  ["VK_UP", "s2", true], // keypress
-  ["VK_UP", "d2", true], // keypress
-  ["VK_UP", "p1", true],
-  ["VK_BACK_SPACE", "p1", false],
-  ["2", "p3", true],
-  ["VK_BACK_SPACE", "p3", false],
-  ["VK_BACK_SPACE", "p3", false],
-  ["VK_BACK_SPACE", "p3", true],
-  ["r", "p3", false],
+  ["#", "b1", true],                 // #
+  ["d", "b1", true],                 // #d
+  ["1", "b1", true],                 // #d1
+  ["VK_RETURN", "d1", true],         // #d1
+  ["VK_BACK_SPACE", "d1", true],     // #d
+  ["2", "d1", true],                 // #d2
+  ["VK_RETURN", "d2", true],         // #d2
+  ["2", "d2", true],                 // #d22
+  ["VK_RETURN", "d2", false],        // #d22
+  ["VK_BACK_SPACE", "d2", false],    // #d2
+  ["VK_RETURN", "d2", true],         // #d2
+  ["VK_BACK_SPACE", "d2", true],     // #d
+  ["1", "d2", true],                 // #d1
+  ["VK_RETURN", "d1", true],         // #d1
+  ["VK_BACK_SPACE", "d1", true],     // #d
+  ["VK_BACK_SPACE", "d1", true],     // #
+  ["VK_BACK_SPACE", "d1", true],     //
+  ["d", "d1", true],                 // d
+  ["i", "d1", true],                 // di
+  ["v", "d1", true],                 // div
+  [".", "d1", true],                 // div.
+  ["c", "d1", true],                 // div.c
+  ["VK_UP", "d1", true],             // div.c1
+  ["VK_TAB", "d1", true],            // div.c1
+  ["VK_RETURN", "d2", true],         // div.c1
+  ["VK_BACK_SPACE", "d2", true],     // div.c
+  ["VK_BACK_SPACE", "d2", true],     // div.
+  ["VK_BACK_SPACE", "d2", true],     // div
+  ["VK_BACK_SPACE", "d2", true],     // di
+  ["VK_BACK_SPACE", "d2", true],     // d
+  ["VK_BACK_SPACE", "d2", true],     //
+  [".", "d2", true],                 // .
+  ["c", "d2", true],                 // .c
+  ["1", "d2", true],                 // .c1
+  ["VK_RETURN", "d2", true],         // .c1
+  ["VK_RETURN", "s2", true],         // .c1
+  ["VK_RETURN", "p1", true],         // .c1
+  ["P", "p1", true],                 // .c1P
+  ["VK_RETURN", "p1", false],        // .c1P
 ];
 
 add_task(function* () {
   let { inspector } = yield openInspectorForURL(TEST_URL);
   let { searchBox } = inspector;
 
   yield selectNode("#b1", inspector);
   yield focusSearchBoxUsingShortcut(inspector.panelWin);
 
   let index = 0;
   for (let [ key, id, isValid ] of KEY_STATES) {
-    let event = (LISTEN_KEYPRESS.indexOf(index) !== -1) ? "keypress" : "command";
-    let eventHandled = once(searchBox, event, true);
+    info(index + ": Pressing key " + key + " to get id " + id + ".");
+    let done = inspector.searchSuggestions.once("processing-done");
+    EventUtils.synthesizeKey(key, {}, inspector.panelWin);
+    yield done;
+    info("Got processing-done event");
 
-    info(index + ": Pressing key " + key + " to get id " + id);
-    EventUtils.synthesizeKey(key, {}, inspector.panelWin);
-    yield eventHandled;
+    if (key === "VK_RETURN") {
+      info ("Waiting for " + (isValid ? "NO " : "") + "results");
+      yield inspector.search.once("search-result");
+    }
 
-    info("Got " + event + " event. Waiting for search query to complete");
+    info("Waiting for search query to complete");
     yield inspector.searchSuggestions._lastQuery;
 
     info(inspector.selection.nodeFront.id + " is selected with text " +
          searchBox.value);
     let nodeFront = yield getNodeFront("#" + id, inspector);
     is(inspector.selection.nodeFront, nodeFront,
        "Correct node is selected for state " + index);
 
--- a/devtools/client/inspector/test/browser_inspector_search-02.js
+++ b/devtools/client/inspector/test/browser_inspector_search-02.js
@@ -11,84 +11,89 @@ const TEST_URL = TEST_URL_ROOT + "doc_in
 // An array of (key, suggestions) pairs where key is a key to press and
 // suggestions is an array of suggestions that should be shown in the popup.
 // Suggestion is an object with label of the entry and optional count
 // (defaults to 1)
 const TEST_DATA = [
   {
     key: "d",
     suggestions: [
-      {label: "div", count: 4},
-      {label: "#d1", count: 1},
-      {label: "#d2", count: 1}
+      {label: "div"},
+      {label: "#d1"},
+      {label: "#d2"}
     ]
   },
   {
     key: "i",
-    suggestions: [{label: "div", count: 4}]
+    suggestions: [{label: "div"}]
   },
   {
     key: "v",
     suggestions: []
   },
   {
     key: " ",
     suggestions: [
-      {label: "div div", count: 2},
-      {label: "div span", count: 2}
+      {label: "div div"},
+      {label: "div span"}
     ]
   },
   {
     key: ">",
     suggestions: [
-      {label: "div >div", count: 2},
-      {label: "div >span", count: 2}
+      {label: "div >div"},
+      {label: "div >span"}
     ]
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: [
-      {label: "div div", count: 2 },
-      {label: "div span", count: 2}
+      {label: "div div"},
+      {label: "div span"}
     ]
   },
   {
     key: "+",
     suggestions: [{label: "div +span"}]
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: [
-      {label: "div div", count: 2 },
-      {label: "div span", count: 2}
+      {label: "div div"},
+      {label: "div span"}
     ]
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: []
   },
   {
     key: "VK_BACK_SPACE",
-    suggestions: [{label: "div", count: 4}]
+    suggestions: [{label: "div"}]
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: [
-      {label: "div", count: 4},
-      {label: "#d1", count: 1},
-      {label: "#d2", count: 1}
+      {label: "div"},
+      {label: "#d1"},
+      {label: "#d2"}
     ]
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: []
   },
   {
     key: "p",
-    suggestions: []
+    suggestions: [
+      {label: "p"},
+      {label: "#p1"},
+      {label: "#p2"},
+      {label: "#p3"},
+    ]
   },
   {
     key: " ",
     suggestions: [{label: "p strong"}]
   },
   {
     key: "+",
     suggestions: [
@@ -148,19 +153,17 @@ add_task(function* () {
     let actualSuggestions = popup.getItems().reverse();
 
     is(popup.isOpen ? actualSuggestions.length: 0, suggestions.length,
        "There are expected number of suggestions.");
 
     for (let i = 0; i < suggestions.length; i++) {
       is(actualSuggestions[i].label, suggestions[i].label,
          "The suggestion at " + i + "th index is correct.");
-      is(actualSuggestions[i].count, suggestions[i].count || 1,
-         "The count for suggestion at " + i + "th index is correct.");
     }
   }
 });
 
 function formatSuggestions(suggestions) {
   return "[" + suggestions
-                .map(s => "'" + s.label + "' (" + (s.count || 1) + ")")
+                .map(s => "'" + s.label + "'")
                 .join(", ") + "]";
 }
--- a/devtools/client/inspector/test/browser_inspector_search-03.js
+++ b/devtools/client/inspector/test/browser_inspector_search-03.js
@@ -11,24 +11,24 @@ const TEST_URL = TEST_URL_ROOT + "doc_in
 // An array of (key, suggestions) pairs where key is a key to press and
 // suggestions is an array of suggestions that should be shown in the popup.
 // Suggestion is an object with label of the entry and optional count
 // (defaults to 1)
 var TEST_DATA = [
   {
     key: "d",
     suggestions: [
-      {label: "div", count: 2},
-      {label: "#d1", count: 1},
-      {label: "#d2", count: 1}
+      {label: "div"},
+      {label: "#d1"},
+      {label: "#d2"}
     ]
   },
   {
     key: "i",
-    suggestions: [{label: "div", count: 2}]
+    suggestions: [{label: "div"}]
   },
   {
     key: "v",
     suggestions: []
   },
   {
     key: ".",
     suggestions: [{label: "div.c1"}]
@@ -45,52 +45,52 @@ var TEST_DATA = [
     ]
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: []
   },
   {
     key: "VK_BACK_SPACE",
-    suggestions: [{label: "div", count: 2}]
+    suggestions: [{label: "div"}]
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: [
-      {label: "div", count: 2},
-      {label: "#d1", count: 1},
-      {label: "#d2", count: 1}
+      {label: "div"},
+      {label: "#d1"},
+      {label: "#d2"}
     ]
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: []
   },
   {
     key: ".",
     suggestions: [
-      {label: ".c1", count: 3},
+      {label: ".c1"},
       {label: ".c2"}
     ]
   },
   {
     key: "c",
     suggestions: [
-      {label: ".c1", count: 3},
+      {label: ".c1"},
       {label: ".c2"}
     ]
   },
   {
     key: "2",
     suggestions: []
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: [
-      {label: ".c1", count: 3},
+      {label: ".c1"},
       {label: ".c2"}
     ]
   },
   {
     key: "1",
     suggestions: []
   },
   {
@@ -103,24 +103,24 @@ var TEST_DATA = [
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: []
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: [
-      {label: ".c1", count: 3},
+      {label: ".c1"},
       {label: ".c2"}
     ]
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: [
-      {label: ".c1", count: 3},
+      {label: ".c1"},
       {label: ".c2"}
     ]
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: []
   },
   {
@@ -184,19 +184,17 @@ add_task(function* () {
     let actualSuggestions = popup.getItems().reverse();
 
     is(popup.isOpen ? actualSuggestions.length: 0, suggestions.length,
        "There are expected number of suggestions.");
 
     for (let i = 0; i < suggestions.length; i++) {
       is(actualSuggestions[i].label, suggestions[i].label,
          "The suggestion at " + i + "th index is correct.");
-      is(actualSuggestions[i].count, suggestions[i].count || 1,
-         "The count for suggestion at " + i + "th index is correct.");
     }
   }
 });
 
 function formatSuggestions(suggestions) {
   return "[" + suggestions
-                .map(s => "'" + s.label + "' (" + (s.count || 1) + ")")
+                .map(s => "'" + s.label + "'")
                 .join(", ") + "]";
 }
--- a/devtools/client/inspector/test/browser_inspector_search-04.js
+++ b/devtools/client/inspector/test/browser_inspector_search-04.js
@@ -14,67 +14,67 @@ const TEST_URL = "data:text/html;charset
 // An array of (key, suggestions) pairs where key is a key to press and
 // suggestions is an array of suggestions that should be shown in the popup.
 // Suggestion is an object with label of the entry and optional count
 // (defaults to 1)
 var TEST_DATA = [
   {
     key: "d",
     suggestions: [
-      {label: "div", count: 5},
-      {label: "#d1", count: 2},
-      {label: "#d2", count: 2}
+      {label: "div"},
+      {label: "#d1"},
+      {label: "#d2"}
     ]
   },
   {
     key: "i",
-    suggestions: [{label: "div", count: 5}]
+    suggestions: [{label: "div"}]
   },
   {
     key: "v",
     suggestions: []
   },
   {
     key: "VK_BACK_SPACE",
-    suggestions: [{label: "div", count: 5}]
+    suggestions: [{label: "div"}]
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: [
-      {label: "div", count: 5},
-      {label: "#d1", count: 2},
-      {label: "#d2", count: 2}
+      {label: "div"},
+      {label: "#d1"},
+      {label: "#d2"}
     ]
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: []
   },
   {
     key: ".",
     suggestions: [
-      {label: ".c1", count: 7},
-      {label: ".c2", count: 3}
+      {label: ".c1"},
+      {label: ".c2"}
     ]
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: []
   },
   {
     key: "#",
     suggestions: [
-      {label: "#b1", count: 2},
-      {label: "#d1", count: 2},
-      {label: "#d2", count: 2},
-      {label: "#p1", count: 2},
-      {label: "#p2", count: 2},
-      {label: "#p3", count: 2},
-      {label: "#s1", count: 2},
-      {label: "#s2", count: 2}
+      {label: "#b1"},
+      {label: "#d1"},
+      {label: "#d2"},
+      {label: "#p1"},
+      {label: "#p2"},
+      {label: "#p3"},
+      {label: "#s1"},
+      {label: "#s2"}
     ]
   },
 ];
 
 add_task(function* () {
   let {inspector} = yield openInspectorForURL(TEST_URL);
   let searchBox = inspector.searchBox;
   let popup = inspector.searchSuggestions.searchPopup;
@@ -95,19 +95,17 @@ add_task(function* () {
     let actualSuggestions = popup.getItems().reverse();
 
     is(popup.isOpen ? actualSuggestions.length: 0, suggestions.length,
        "There are expected number of suggestions.");
 
     for (let i = 0; i < suggestions.length; i++) {
       is(actualSuggestions[i].label, suggestions[i].label,
          "The suggestion at " + i + "th index is correct.");
-      is(actualSuggestions[i].count, suggestions[i].count || 1,
-         "The count for suggestion at " + i + "th index is correct.");
     }
   }
 });
 
 function formatSuggestions(suggestions) {
   return "[" + suggestions
-                .map(s => "'" + s.label + "' (" + (s.count || 1) + ")")
+                .map(s => "'" + s.label + "'")
                 .join(", ") + "]";
 }
--- a/devtools/client/inspector/test/browser_inspector_search-05.js
+++ b/devtools/client/inspector/test/browser_inspector_search-05.js
@@ -6,59 +6,86 @@
 // Testing that when search results contain suggestions for nodes in other
 // frames, selecting these suggestions actually selects the right nodes.
 
 const IFRAME_SRC = "doc_inspector_search.html";
 const TEST_URL = "data:text/html;charset=utf-8," +
                  "<iframe id=\"iframe-1\" src=\"" +
                  TEST_URL_ROOT + IFRAME_SRC + "\"></iframe>" +
                  "<iframe id=\"iframe-2\" src=\"" +
-                 TEST_URL_ROOT + IFRAME_SRC + "\"></iframe>";
+                 TEST_URL_ROOT + IFRAME_SRC + "\"></iframe>" +
+                 "<iframe id='iframe-3' src='data:text/html," +
+                   "<button id=\"b1\">Nested button</button>" +
+                   "<iframe id=\"iframe-4\" src=" + TEST_URL_ROOT + IFRAME_SRC + "></iframe>'>" +
+                 "</iframe>";
 
 add_task(function* () {
   let {inspector} = yield openInspectorForURL(TEST_URL);
-  let {walker} = inspector;
 
   let searchBox = inspector.searchBox;
   let popup = inspector.searchSuggestions.searchPopup;
 
   info("Focus the search box");
   yield focusSearchBoxUsingShortcut(inspector.panelWin);
 
   info("Enter # to search for all ids");
-  let command = once(searchBox, "command");
+  let processingDone = once(inspector.searchSuggestions, "processing-done");
   EventUtils.synthesizeKey("#", {}, inspector.panelWin);
-  yield command;
+  yield processingDone;
 
   info("Wait for search query to complete");
   yield inspector.searchSuggestions._lastQuery;
 
-  info("Press tab to fill the search input with the first suggestion and " +
-    "expect a new selection");
+  info("Press tab to fill the search input with the first suggestion");
+  processingDone = once(inspector.searchSuggestions, "processing-done");
+  EventUtils.synthesizeKey("VK_TAB", {}, inspector.panelWin);
+  yield processingDone;
+
+  info("Press enter and expect a new selection");
   let onSelect = inspector.once("inspector-updated");
-  EventUtils.synthesizeKey("VK_TAB", {}, inspector.panelWin);
+  EventUtils.synthesizeKey("VK_RETURN", {}, inspector.panelWin);
   yield onSelect;
 
-  let node = inspector.selection.nodeFront;
-  ok(node.id, "b1", "The selected node is #b1");
-  ok(node.tagName.toLowerCase(), "button",
-    "The selected node is <button>");
+  yield checkCorrectButton(inspector, "#iframe-1");
 
-  let selectedNodeDoc = yield walker.document(node);
-  let iframe1 = yield walker.querySelector(walker.rootNode, "#iframe-1");
-  let iframe1Doc = (yield walker.children(iframe1)).nodes[0];
-  is(selectedNodeDoc, iframe1Doc, "The selected node is in iframe 1");
+  info("Press enter to cycle through multiple nodes matching this suggestion");
+  onSelect = inspector.once("inspector-updated");
+  EventUtils.synthesizeKey("VK_RETURN", {}, inspector.panelWin);
+  yield onSelect;
+
+  yield checkCorrectButton(inspector, "#iframe-2");
 
   info("Press enter to cycle through multiple nodes matching this suggestion");
   onSelect = inspector.once("inspector-updated");
   EventUtils.synthesizeKey("VK_RETURN", {}, inspector.panelWin);
   yield onSelect;
 
-  node = inspector.selection.nodeFront;
-  ok(node.id, "b1", "The selected node is #b1 again");
-  ok(node.tagName.toLowerCase(), "button",
-    "The selected node is <button> again");
+  yield checkCorrectButton(inspector, "#iframe-3");
+
+  info("Press enter to cycle through multiple nodes matching this suggestion");
+  onSelect = inspector.once("inspector-updated");
+  EventUtils.synthesizeKey("VK_RETURN", {}, inspector.panelWin);
+  yield onSelect;
+
+  yield checkCorrectButton(inspector, "#iframe-4");
+
+  info("Press enter to cycle through multiple nodes matching this suggestion");
+  onSelect = inspector.once("inspector-updated");
+  EventUtils.synthesizeKey("VK_RETURN", {}, inspector.panelWin);
+  yield onSelect;
 
-  selectedNodeDoc = yield walker.document(node);
-  let iframe2 = yield walker.querySelector(walker.rootNode, "#iframe-2");
-  let iframe2Doc = (yield walker.children(iframe2)).nodes[0];
-  is(selectedNodeDoc, iframe2Doc, "The selected node is in iframe 2");
+  yield checkCorrectButton(inspector, "#iframe-1");
 });
+
+let checkCorrectButton = Task.async(function*(inspector, frameSelector) {
+  let {walker} = inspector;
+  let node = inspector.selection.nodeFront;
+
+  ok(node.id, "b1", "The selected node is #b1");
+  ok(node.tagName.toLowerCase(), "button",
+    "The selected node is <button>");
+
+  let selectedNodeDoc = yield walker.document(node);
+  let iframe = yield walker.multiFrameQuerySelectorAll(frameSelector);
+  iframe = yield iframe.item(0);
+  let iframeDoc = (yield walker.children(iframe)).nodes[0];
+  is(selectedNodeDoc, iframeDoc, "The selected node is in " + frameSelector);
+});
--- a/devtools/client/inspector/test/browser_inspector_search-06.js
+++ b/devtools/client/inspector/test/browser_inspector_search-06.js
@@ -9,43 +9,48 @@
 
 const TEST_URL = TEST_URL_ROOT + "doc_inspector_search.html";
 
 add_task(function* () {
   let { inspector, testActor } = yield openInspectorForURL(TEST_URL);
 
   info("Searching for test node #d1");
   yield focusSearchBoxUsingShortcut(inspector.panelWin);
-  yield synthesizeKeys(["#", "d", "1"], inspector);
+  yield synthesizeKeys(["#", "d", "1", "VK_RETURN"], inspector);
 
+  yield inspector.search.once("search-result");
   assertHasResult(inspector, true);
 
   info("Removing node #d1");
   yield mutatePage(inspector, testActor,
                    "document.getElementById(\"d1\").remove()");
 
   info("Pressing return button to search again for node #d1.");
   yield synthesizeKeys("VK_RETURN", inspector);
 
+  yield inspector.search.once("search-result");
   assertHasResult(inspector, false);
 
   info("Emptying the field and searching for a node that doesn't exist: #d3");
-  let keys = ["VK_BACK_SPACE", "VK_BACK_SPACE", "VK_BACK_SPACE", "#", "d", "3"];
+  let keys = ["VK_BACK_SPACE", "VK_BACK_SPACE", "VK_BACK_SPACE", "#", "d", "3",
+              "VK_RETURN"];
   yield synthesizeKeys(keys, inspector);
 
+  yield inspector.search.once("search-result");
   assertHasResult(inspector, false);
 
   info("Create the #d3 node in the page");
   yield mutatePage(inspector, testActor,
                    `document.getElementById("d2").insertAdjacentHTML(
                     "afterend", "<div id=d3></div>")`);
 
   info("Pressing return button to search again for node #d3.");
   yield synthesizeKeys("VK_RETURN", inspector);
 
+  yield inspector.search.once("search-result");
   assertHasResult(inspector, true);
 
   // Catch-all event for remaining server requests when searching for the new
   // node.
   yield inspector.once("inspector-updated");
 });
 
 function* synthesizeKeys(keys, inspector) {
--- a/devtools/client/inspector/test/browser_inspector_search-reserved.js
+++ b/devtools/client/inspector/test/browser_inspector_search-reserved.js
@@ -8,95 +8,95 @@ const TEST_URL = TEST_URL_ROOT + "doc_in
 
 // An array of (key, suggestions) pairs where key is a key to press and
 // suggestions is an array of suggestions that should be shown in the popup.
 // Suggestion is an object with label of the entry and optional count
 // (defaults to 1)
 const TEST_DATA = [
   {
     key: "#",
-    suggestions: [{label: "#d1\\.d2", count: 1}]
+    suggestions: [{label: "#d1\\.d2"}]
   },
   {
     key: "d",
-    suggestions: [{label: "#d1\\.d2", count: 1}]
+    suggestions: [{label: "#d1\\.d2"}]
   },
   {
     key: "VK_BACK_SPACE",
-    suggestions: [{label: "#d1\\.d2", count: 1}]
+    suggestions: [{label: "#d1\\.d2"}]
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: []
   },
   {
     key: ".",
-    suggestions: [{label: ".c1\\.c2", count: 1}]
+    suggestions: [{label: ".c1\\.c2"}]
   },
   {
     key: "c",
-    suggestions: [{label: ".c1\\.c2", count: 1}]
+    suggestions: [{label: ".c1\\.c2"}]
   },
   { 
     key: "VK_BACK_SPACE", 
-    suggestions: [{label: ".c1\\.c2", count: 1}]
+    suggestions: [{label: ".c1\\.c2"}]
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: []
   },
   {
     key: "d", 
-    suggestions: [{label: "div", count: 2},
-                  {label: "#d1\\.d2", count: 1}]
+    suggestions: [{label: "div"},
+                  {label: "#d1\\.d2"}]
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: []
   },
   {
     key:"c",
-    suggestions: [{label: ".c1\\.c2", count: 1}]
+    suggestions: [{label: ".c1\\.c2"}]
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: []
   },
   {
     key: "b",
-    suggestions: [{label: "body", count: 1}]
+    suggestions: [{label: "body"}]
   }, 
   {
     key: "o",
-    suggestions: [{label: "body", count: 1}]
+    suggestions: [{label: "body"}]
   },
   {
     key: "d",
-    suggestions: [{label: "body", count: 1}]
+    suggestions: [{label: "body"}]
   },
   { 
     key: "y",
     suggestions: []
   }, 
   {
     key: " ",
-    suggestions: [{label: "body div", count: 2}]
+    suggestions: [{label: "body div"}]
   },
   {
     key: ".",
-    suggestions: [{label: "body .c1\\.c2", count: 1}]
+    suggestions: [{label: "body .c1\\.c2"}]
   },
   {
     key: "VK_BACK_SPACE",
-    suggestions: [{label: "body div", count: 2}]
+    suggestions: [{label: "body div"}]
   },
   {
     key: "#",
-    suggestions: [{label: "body #", count: 1},
-                  {label: "body #d1\\.d2", count: 1}]
+    suggestions: [{label: "body #"},
+                  {label: "body #d1\\.d2"}]
   }
 ];
 
 add_task(function* () {
   let { inspector } = yield openInspectorForURL(TEST_URL);
   let searchBox = inspector.searchBox;
   let popup = inspector.searchSuggestions.searchPopup;
 
@@ -116,19 +116,17 @@ add_task(function* () {
     let actualSuggestions = popup.getItems().reverse();
 
     is(popup.isOpen ? actualSuggestions.length: 0, suggestions.length,
        "There are expected number of suggestions.");
 
     for (let i = 0; i < suggestions.length; i++) {
       is(suggestions[i].label, actualSuggestions[i].label,
          "The suggestion at " + i + "th index is correct.");
-      is(suggestions[i].count || 1, actualSuggestions[i].count,
-         "The count for suggestion at " + i + "th index is correct.");
     }
   }
 });
 
 function formatSuggestions(suggestions) {
   return "[" + suggestions
-                .map(s => "'" + s.label + "' (" + s.count || 1 + ")")
+                .map(s => "'" + s.label + "'")
                 .join(", ") + "]";
 }
--- a/devtools/client/locales/en-US/inspector.dtd
+++ b/devtools/client/locales/en-US/inspector.dtd
@@ -108,19 +108,18 @@
 
 <!ENTITY inspector.selectButton.tooltip     "Select element with mouse">
 
 <!-- LOCALIZATION NOTE (inspectorSearchHTML.label2): This is the label shown as
      the placeholder in inspector search box -->
 <!ENTITY inspectorSearchHTML.label2          "Search with CSS Selectors">
 <!ENTITY inspectorSearchHTML.key            "F">
 
-<!-- LOCALIZATION NOTE (inspectorSearchHTML.label3): This is the label that will
-     be shown as the placeholder in the future, once the inspector search box
-     supports the full text HTML search in Bug 835896. -->
+<!-- LOCALIZATION NOTE (inspectorSearchHTML.label3): This is the label that is
+     shown as the placeholder for the markup view search in the inspector. -->
 <!ENTITY inspectorSearchHTML.label3 "Search HTML">
 
 <!-- LOCALIZATION NOTE (inspectorCopyImageDataUri.label): This is the label
      shown in the inspector contextual-menu for the item that lets users copy
      the URL embedding the image data encoded in Base 64 (what we name
      here Image Data URL). For more information:
      https://developer.mozilla.org/en-US/docs/Web/HTTP/data_URIs -->
 <!ENTITY inspectorCopyImageDataUri.label       "Copy Image Data-URL">
--- a/devtools/client/memory/components/toolbar.js
+++ b/devtools/client/memory/components/toolbar.js
@@ -35,27 +35,27 @@ const Toolbar = module.exports = createC
       filterString,
       setFilterString
     } = this.props;
 
     return (
       dom.div({ className: "devtools-toolbar" },
         dom.button({
           id: "take-snapshot",
-          className: `take-snapshot devtools-button`,
+          className: "take-snapshot devtools-button",
           onClick: onTakeSnapshotClick,
           title: L10N.getStr("take-snapshot")
         }),
 
         dom.div({ className: "toolbar-group" },
           dom.label({ className: "breakdown-by" },
             L10N.getStr("toolbar.breakdownBy"),
             dom.select({
               id: "select-breakdown",
-              className: `select-breakdown`,
+              className: "select-breakdown",
               onChange: e => onBreakdownChange(e.target.value),
             }, ...breakdowns.map(({ name, displayName }) => dom.option({ key: name, value: name }, displayName)))
           ),
 
           dom.label({},
             dom.input({
               id: "invert-tree-checkbox",
               type: "checkbox",
@@ -76,16 +76,17 @@ const Toolbar = module.exports = createC
             L10N.getStr("checkbox.recordAllocationStacks")
           ),
 
           dom.div({ id: "toolbar-spacer", className: "spacer" }),
 
           dom.input({
             id: "filter",
             type: "search",
+            className: "devtools-searchinput",
             placeholder: L10N.getStr("filter.placeholder"),
             onChange: event => setFilterString(event.target.value),
             value: !!filterString ? filterString : undefined,
           })
         )
       )
     );
   }
--- a/devtools/client/shared/test/browser_filter-editor-01.js
+++ b/devtools/client/shared/test/browser_filter-editor-01.js
@@ -54,9 +54,19 @@ add_task(function *() {
   widget.setCssValue("drop-shadow(whatever)");
   is(widget.getCssValue(), "drop-shadow()",
      "setCssValue should replace invalid drop-shadow argument with empty string");
 
   info("Test parsing of mixed invalid argument");
   widget.setCssValue("contrast(5%) whatever invert('xxx')");
   is(widget.getCssValue(), "contrast(5%) invert(0%)",
      "setCssValue should handle multiple errors");
+
+  info("Test parsing of 'unset'");
+  widget.setCssValue("unset");
+  is(widget.getCssValue(), "unset", "setCssValue should handle 'unset'");
+  info("Test parsing of 'initial'");
+  widget.setCssValue("initial");
+  is(widget.getCssValue(), "initial", "setCssValue should handle 'initial'");
+  info("Test parsing of 'inherit'");
+  widget.setCssValue("inherit");
+  is(widget.getCssValue(), "inherit", "setCssValue should handle 'inherit'");
 });
--- a/devtools/client/shared/widgets/FilterWidget.js
+++ b/devtools/client/shared/widgets/FilterWidget.js
@@ -93,16 +93,19 @@ const filterList = [
   },
   {
     "name": "url",
     "placeholder": "example.svg#c1",
     "type": "string"
   }
 ];
 
+// Valid values that shouldn't be parsed for filters.
+const SPECIAL_VALUES = new Set(["none", "unset", "initial", "inherit"]);
+
 /**
  * A CSS Filter editor widget used to add/remove/modify
  * filters.
  *
  * Normally, it takes a CSS filter value as input, parses it
  * and creates the required elements / bindings.
  *
  * You can, however, use add/remove/update methods manually.
@@ -552,17 +555,17 @@ CSSFilterEditorWidget.prototype = {
   },
 
   _savePreset: function(e) {
     e.preventDefault();
 
     let name = this.addPresetInput.value;
     let value = this.getCssValue();
 
-    if (!name || !value || value === "none") {
+    if (!name || !value || SPECIAL_VALUES.has(value)) {
       this.emit("preset-save-error");
       return;
     }
 
     this.getPresets().then(presets => {
       let index = presets.findIndex(preset => preset.name === name);
 
       if (index > -1) {
@@ -701,17 +704,18 @@ CSSFilterEditorWidget.prototype = {
     */
   setCssValue: function(cssValue) {
     if (!cssValue) {
       throw new Error("Missing CSS filter value in setCssValue");
     }
 
     this.filters = [];
 
-    if (cssValue === "none") {
+    if (SPECIAL_VALUES.has(cssValue)) {
+      this._specialValue = cssValue;
       this.emit("updated", this.getCssValue());
       this.render();
       return;
     }
 
     for (let {name, value} of tokenizeFilterValue(cssValue)) {
       // If the specified value is invalid, replace it with the
       // default.
@@ -820,17 +824,17 @@ CSSFilterEditorWidget.prototype = {
     * Generates CSS filter value for filters of the widget
     *
     * @return {String}
     *        css value of filters
     */
   getCssValue: function() {
     return this.filters.map((filter, i) => {
       return `${filter.name}(${this.getValueAt(i)})`;
-    }).join(" ") || "none";
+    }).join(" ") || this._specialValue || "none";
   },
 
   /**
     * Updates specified filter's value
     *
     * @param {Number} index
     *        The index of the filter in the current list of filters
     * @param {number/string} value
@@ -901,17 +905,17 @@ function swapArrayIndices(array, a, b) {
  *
  * @param {String} css CSS Filter value to be parsed
  * @return {Array} An array of {name, value} pairs
  */
 function tokenizeFilterValue(css) {
   let filters = [];
   let depth = 0;
 
-  if (css === "none") {
+  if (SPECIAL_VALUES.has(css)) {
     return filters;
   }
 
   let state = "initial";
   let name;
   let contents;
   for (let token of cssTokenizer(css)) {
     switch (state) {
--- a/devtools/client/themes/inspector.css
+++ b/devtools/client/themes/inspector.css
@@ -10,16 +10,20 @@
 }
 
 #inspector-toolbar:-moz-locale-dir(rtl) {
   padding-left: 4px;
 }
 
 %endif
 
+#inspector-searchlabel {
+  overflow: hidden;
+}
+
 #inspector-searchbox {
   transition-property: max-width, -moz-padding-end, -moz-padding-start;
   transition-duration: 250ms;
   transition-timing-function: ease;
 }
 
 #inspector-searchbox:not([focused]):not([filled]) > .textbox-input-box {
   overflow: hidden;
--- a/devtools/client/themes/memory.css
+++ b/devtools/client/themes/memory.css
@@ -95,16 +95,21 @@ html, body, #app, #memory-tool {
     background-image: url(images/command-screenshot@2x.png);
   }
 }
 
 .spacer {
   flex: 1;
 }
 
+#filter {
+  align-self: stretch;
+  margin: 2px;
+}
+
 /**
  * TODO bug 1213100
  * Once we figure out how to store invertable buttons (pseudo element like in
  * this case?) we should add a .invertable class to handle this generally,
  * rather than the definitions in toolbars.css.
  *
  * @see bug 1173397 for another inverted related bug
  */
--- a/devtools/server/actors/inspector.js
+++ b/devtools/server/actors/inspector.js
@@ -56,16 +56,17 @@ const protocol = require("devtools/serve
 const {Arg, Option, method, RetVal, types} = protocol;
 const {LongStringActor, ShortLongString} = require("devtools/server/actors/string");
 const promise = require("promise");
 const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
 const object = require("sdk/util/object");
 const events = require("sdk/event/core");
 const {Unknown} = require("sdk/platform/xpcom");
 const {Class} = require("sdk/core/heritage");
+const {WalkerSearch} = require("devtools/server/actors/utils/walker-search");
 const {PageStyleActor, getFontPreviewData} = require("devtools/server/actors/styles");
 const {
   HighlighterActor,
   CustomHighlighterActor,
   isTypeRegistered,
 } = require("devtools/server/actors/highlighters");
 const {
   isAnonymous,
@@ -1117,16 +1118,23 @@ types.addDictType("disconnectedNodeArray
   nodes: "array:domnode",
 
   // Nodes that are needed to connect those nodes to the root.
   newParents: "array:domnode"
 });
 
 types.addDictType("dommutation", {});
 
+types.addDictType("searchresult", {
+  list: "domnodelist",
+  // Right now there is isn't anything required for metadata,
+  // but it's json so it can be extended with extra data.
+  metadata: "array:json"
+});
+
 /**
  * Server side of a node list as returned by querySelectorAll()
  */
 var NodeListActor = exports.NodeListActor = protocol.ActorClass({
   typeName: "domnodelist",
 
   initialize: function(walker, nodeList) {
     protocol.Actor.prototype.initialize.call(this);
@@ -1294,16 +1302,18 @@ var WalkerActor = protocol.ActorClass({
     this.tabActor = tabActor;
     this.rootWin = tabActor.window;
     this.rootDoc = this.rootWin.document;
     this._refMap = new Map();
     this._pendingMutations = [];
     this._activePseudoClassLocks = new Set();
     this.showAllAnonymousContent = options.showAllAnonymousContent;
 
+    this.walkerSearch = new WalkerSearch(this);
+
     // Nodes which have been removed from the client's known
     // ownership tree are considered "orphaned", and stored in
     // this set.
     this._orphaned = new Set();
 
     // The client can tell the walker that it is interested in a node
     // even when it is orphaned with the `retainNode` method.  This
     // list contains orphaned nodes that were so retained.
@@ -1329,17 +1339,23 @@ var WalkerActor = protocol.ActorClass({
   form: function() {
     return {
       actor: this.actorID,
       root: this.rootNode.form(),
       traits: {
         // FF42+ Inspector starts managing the Walker, while the inspector also
         // starts cleaning itself up automatically on client disconnection.
         // So that there is no need to manually release the walker anymore.
-        autoReleased: true
+        autoReleased: true,
+        // XXX: It seems silly that we need to tell the front which capabilities
+        // its actor has in this way when the target can use actorHasMethod.  If
+        // this was ported to the protocol (Bug 1157048) we could call that inside
+        // of custom front methods and not need to do traits for this.
+        multiFrameQuerySelectorAll: true,
+        textSearch: true,
       }
     }
   },
 
   toString: function() {
     return "[WalkerActor " + this.actorID + "]";
   },
 
@@ -1371,16 +1387,17 @@ var WalkerActor = protocol.ActorClass({
       this._refMap = null;
 
       events.off(this.tabActor, "will-navigate", this.onFrameUnload);
       events.off(this.tabActor, "navigate", this.onFrameLoad);
 
       this.onFrameLoad = null;
       this.onFrameUnload = null;
 
+      this.walkerSearch.destroy();
       this.reflowObserver.off("reflows", this._onReflows);
       this.reflowObserver = null;
       this._onReflows = null;
       releaseLayoutChangesObserver(this.tabActor);
 
       this.onMutations = null;
 
       this.tabActor = null;
@@ -2044,16 +2061,43 @@ var WalkerActor = protocol.ActorClass({
       selector: Arg(0)
     },
     response: {
       list: RetVal("domnodelist")
     }
   }),
 
   /**
+   * Search the document for a given string.
+   * Results will be searched with the walker-search module (searches through
+   * tag names, attribute names and values, and text contents).
+   *
+   * @returns {searchresult}
+   *            - {NodeList} list
+   *            - {Array<Object>} metadata. Extra information with indices that
+   *                              match up with node list.
+   */
+  search: method(function(query) {
+    let results = this.walkerSearch.search(query);
+    let nodeList = new NodeListActor(this, results.map(r => r.node));
+
+    return {
+      list: nodeList,
+      metadata: []
+    }
+  }, {
+    request: {
+      query: Arg(0),
+    },
+    response: {
+      list: RetVal("searchresult"),
+    }
+  }),
+
+  /**
    * Returns a list of matching results for CSS selector autocompletion.
    *
    * @param string query
    *        The selector query being completed
    * @param string completing
    *        The exact token being completed out of the query
    * @param string selectorState
    *        One of "pseudo", "id", "tag", "class", "null"
@@ -2833,16 +2877,21 @@ var WalkerActor = protocol.ActorClass({
 
   /**
    * Handles mutations from the DOM mutation observer API.
    *
    * @param array[MutationRecord] mutations
    *    See https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#MutationRecord
    */
   onMutations: function(mutations) {
+    // Notify any observers that want *all* mutations (even on nodes that aren't
+    // referenced).  This is not sent over the protocol so can only be used by
+    // scripts running in the server process.
+    events.emit(this, "any-mutation");
+
     for (let change of mutations) {
       let targetActor = this._refMap.get(change.target);
       if (!targetActor) {
         continue;
       }
       let targetNode = change.target;
       let type = change.type;
       let mutation = {
@@ -3310,16 +3359,85 @@ var WalkerFront = exports.WalkerFront = 
   getNodeFromActor: protocol.custom(function(actorID, path) {
     return this._getNodeFromActor(actorID, path).then(response => {
       return response ? response.node : null;
     });
   }, {
     impl: "_getNodeFromActor"
   }),
 
+  /*
+   * Incrementally search the document for a given string.
+   * For modern servers, results will be searched with using the WalkerActor
+   * `search` function (includes tag names, attributes, and text contents).
+   * Only 1 result is sent back, and calling the method again with the same
+   * query will send the next result. When there are no more results to be sent
+   * back, null is sent.
+   * @param {String} query
+   * @param {Object} options
+   *    - "reverse": search backwards
+   *    - "selectorOnly": treat input as a selector string (don't search text
+   *                      tags, attributes, etc)
+   */
+  search: protocol.custom(Task.async(function*(query, options = { }) {
+    let nodeList;
+    let searchType;
+    let searchData = this.searchData = this.searchData || { };
+    let selectorOnly = !!options.selectorOnly;
+
+    // Backwards compat.  Use selector only search if the new
+    // search functionality isn't implemented, or if the caller (tests)
+    // want it.
+    if (selectorOnly || !this.traits.textSearch) {
+      searchType = "selector";
+      if (this.traits.multiFrameQuerySelectorAll) {
+        nodeList = yield this.multiFrameQuerySelectorAll(query);
+      } else {
+        nodeList = yield this.querySelectorAll(this.rootNode, query);
+      }
+    } else {
+      searchType = "search";
+      let result = yield this._search(query, options);
+      nodeList = result.list;
+    }
+
+    // If this is a new search, start at the beginning.
+    if (searchData.query !== query ||
+        searchData.selectorOnly !== selectorOnly) {
+      searchData.selectorOnly = selectorOnly;
+      searchData.query = query;
+      searchData.index = -1;
+    }
+
+    if (!nodeList.length) {
+      return null;
+    }
+
+    // Move search result cursor and cycle if necessary.
+    searchData.index = options.reverse ? searchData.index - 1 :
+                                         searchData.index + 1;
+    if (searchData.index >= nodeList.length) {
+      searchData.index = 0;
+    }
+    if (searchData.index < 0) {
+      searchData.index = nodeList.length - 1;
+    }
+
+    // Send back the single node, along with any relevant search data
+    let node = yield nodeList.item(searchData.index);
+    return {
+      type: searchType,
+      node: node,
+      resultsLength: nodeList.length,
+      resultsIndex: searchData.index,
+    };
+  }), {
+    impl: "_search"
+  }),
+
   _releaseFront: function(node, force) {
     if (node.retained && !force) {
       node.reparent(null);
       this._retainedOrphans.add(node);
       return;
     }
 
     if (node.retained) {
@@ -3870,33 +3988,49 @@ DocumentWalker.prototype = {
   set currentNode(aVal) {
     this.walker.currentNode = aVal;
   },
 
   parentNode: function() {
     return this.walker.parentNode();
   },
 
+  nextNode: function() {
+    let node = this.walker.currentNode;
+    if (!node) {
+      return null;
+    }
+
+    let nextNode = this.walker.nextNode();
+    while (nextNode && this.filter(nextNode) === Ci.nsIDOMNodeFilter.FILTER_SKIP) {
+      nextNode = this.walker.nextNode();
+    }
+
+    return nextNode;
+  },
+
   firstChild: function() {
     let node = this.walker.currentNode;
-    if (!node)
+    if (!node) {
       return null;
+    }
 
     let firstChild = this.walker.firstChild();
     while (firstChild && this.filter(firstChild) === Ci.nsIDOMNodeFilter.FILTER_SKIP) {
       firstChild = this.walker.nextSibling();
     }
 
     return firstChild;
   },
 
   lastChild: function() {
     let node = this.walker.currentNode;
-    if (!node)
+    if (!node) {
       return null;
+    }
 
     let lastChild = this.walker.lastChild();
     while (lastChild && this.filter(lastChild) === Ci.nsIDOMNodeFilter.FILTER_SKIP) {
       lastChild = this.walker.previousSibling();
     }
 
     return lastChild;
   },
--- a/devtools/server/actors/styles.js
+++ b/devtools/server/actors/styles.js
@@ -2079,16 +2079,23 @@ function getRuleText(initialText, line, 
     endOffset = token.endOffset;
   }
 
   // If the rule was of the form "selector {" with no closing brace
   // and no properties, just return an empty string.
   if (startOffset === undefined) {
     return {offset: 0, text: ""};
   }
+  // If the input didn't have any tokens between the braces (e.g.,
+  // "div {}"), then the endOffset won't have been set yet; so account
+  // for that here.
+  if (endOffset === undefined) {
+    endOffset = startOffset;
+  }
+
   // Note that this approach will preserve comments, despite the fact
   // that cssTokenizer skips them.
   return {offset: textOffset + startOffset,
           text: text.substring(startOffset, endOffset)};
 }
 
 exports.getRuleText = getRuleText;
 
--- a/devtools/server/actors/utils/moz.build
+++ b/devtools/server/actors/utils/moz.build
@@ -7,10 +7,11 @@
 DevToolsModules(
     'actor-registry-utils.js',
     'audionodes.json',
     'automation-timeline.js',
     'make-debugger.js',
     'map-uri-to-addon-id.js',
     'ScriptStore.js',
     'stack.js',
-    'TabSources.js'
+    'TabSources.js',
+    'walker-search.js'
 )
new file mode 100644
--- /dev/null
+++ b/devtools/server/actors/utils/walker-search.js
@@ -0,0 +1,278 @@
+/* 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";
+
+/**
+ * The walker-search module provides a simple API to index and search strings
+ * and elements inside a given document.
+ * It indexes tag names, attribute names and values, and text contents.
+ * It provides a simple search function that returns a list of nodes that
+ * matched.
+ */
+
+const {Ci, Cu} = require("chrome");
+
+/**
+ * The WalkerIndex class indexes the document (and all subdocs) from
+ * a given walker.
+ *
+ * It is only indexed the first time the data is accessed and will be
+ * re-indexed if a mutation happens between requests.
+ *
+ * @param {Walker} walker The walker to be indexed
+ */
+function WalkerIndex(walker) {
+  this.walker = walker;
+  this.clearIndex = this.clearIndex.bind(this);
+
+  // Kill the index when mutations occur, the next data get will re-index.
+  this.walker.on("any-mutation", this.clearIndex);
+}
+
+WalkerIndex.prototype = {
+  /**
+   * Destroy this instance, releasing all data and references
+   */
+  destroy: function() {
+    this.walker.off("any-mutation", this.clearIndex);
+  },
+
+  clearIndex: function() {
+    if (!this.currentlyIndexing) {
+      this._data = null;
+    }
+  },
+
+  get doc() {
+    return this.walker.rootDoc;
+  },
+
+  /**
+   * Get the indexed data
+   * This getter also indexes if it hasn't been done yet or if the state is
+   * dirty
+   *
+   * @returns Map<String, Array<{type:String, node:DOMNode}>>
+   *          A Map keyed on the searchable value, containing an array with
+   *          objects containing the 'type' (one of ALL_RESULTS_TYPES), and
+   *          the DOM Node.
+   */
+  get data() {
+    if (!this._data) {
+      this._data = new Map();
+      this.index();
+    }
+
+    return this._data;
+  },
+
+  _addToIndex: function(type, node, value) {
+    // Add an entry for this value if there isn't one
+    let entry = this._data.get(value);
+    if (!entry) {
+      this._data.set(value, []);
+    }
+
+    // Add the type/node to the list
+    this._data.get(value).push({
+      type: type,
+      node: node
+    });
+  },
+
+  index: function() {
+    // Handle case where iterating nextNode() with the deepTreeWalker triggers
+    // a mutation (Bug 1222558)
+    this.currentlyIndexing = true;
+
+    let documentWalker = this.walker.getDocumentWalker(this.doc);
+    while (documentWalker.nextNode()) {
+      let node = documentWalker.currentNode;
+
+      if (node.nodeType === 1) {
+        // For each element node, we get the tagname and all attributes names
+        // and values
+        let localName = node.localName;
+        if (localName === "_moz_generated_content_before") {
+          this._addToIndex("tag", node, "::before");
+          this._addToIndex("text", node, node.textContent.trim());
+        } else if (localName === "_moz_generated_content_after") {
+          this._addToIndex("tag", node, "::after");
+          this._addToIndex("text", node, node.textContent.trim());
+        } else {
+          this._addToIndex("tag", node, node.localName);
+        }
+
+        for (let {name, value} of node.attributes) {
+          this._addToIndex("attributeName", node, name);
+          this._addToIndex("attributeValue", node, value);
+        }
+      } else if (node.textContent && node.textContent.trim().length) {
+        // For comments and text nodes, we get the text
+        this._addToIndex("text", node, node.textContent.trim());
+      }
+    }
+
+    this.currentlyIndexing = false;
+  }
+};
+
+exports.WalkerIndex = WalkerIndex;
+
+/**
+ * The WalkerSearch class provides a way to search an indexed document as well
+ * as find elements that match a given css selector.
+ *
+ * Usage example:
+ * let s = new WalkerSearch(doc);
+ * let res = s.search("lang", index);
+ * for (let {matched, results} of res) {
+ *   for (let {node, type} of results) {
+ *     console.log("The query matched a node's " + type);
+ *     console.log("Node that matched", node);
+ *    }
+ * }
+ * s.destroy();
+ *
+ * @param {Walker} the walker to be searched
+ */
+function WalkerSearch(walker) {
+  this.walker = walker;
+  this.index = new WalkerIndex(this.walker);
+}
+
+WalkerSearch.prototype = {
+  destroy: function() {
+    this.index.destroy();
+    this.walker = null;
+  },
+
+  _addResult: function(node, type, results) {
+    if (!results.has(node)) {
+      results.set(node, []);
+    }
+
+    let matches = results.get(node);
+
+    // Do not add if the exact same result is already in the list
+    let isKnown = false;
+    for (let match of matches) {
+      if (match.type === type) {
+        isKnown = true;
+        break;
+      }
+    }
+
+    if (!isKnown) {
+      matches.push({type});
+    }
+  },
+
+  _searchIndex: function(query, options, results) {
+    for (let [matched, res] of this.index.data) {
+      if (!options.searchMethod(query, matched)) {
+        continue;
+      }
+
+      // Add any relevant results (skipping non-requested options).
+      res.filter(entry => {
+        return options.types.indexOf(entry.type) !== -1;
+      }).forEach(({node, type}) => {
+        this._addResult(node, type, results);
+      });
+    }
+  },
+
+  _searchSelectors: function(query, options, results) {
+    // If the query is just one "word", no need to search because _searchIndex
+    // will lead the same results since it has access to tagnames anyway
+    let isSelector = query && query.match(/[ >~.#\[\]]/);
+    if (options.types.indexOf("selector") === -1 || !isSelector) {
+      return;
+    }
+
+    let nodes = this.walker._multiFrameQuerySelectorAll(query);
+    for (let node of nodes) {
+      this._addResult(node, "selector", results);
+    }
+  },
+
+  /**
+   * Search the document
+   * @param {String} query What to search for
+   * @param {Object} options The following options are accepted:
+   * - searchMethod {String} one of WalkerSearch.SEARCH_METHOD_*
+   *   defaults to WalkerSearch.SEARCH_METHOD_CONTAINS (does not apply to
+   *   selector search type)
+   * - types {Array} a list of things to search for (tag, text, attributes, etc)
+   *   defaults to WalkerSearch.ALL_RESULTS_TYPES
+   * @return {Array} An array is returned with each item being an object like:
+   * {
+   *   node: <the dom node that matched>,
+   *   type: <the type of match: one of WalkerSearch.ALL_RESULTS_TYPES>
+   * }
+   */
+  search: function(query, options={}) {
+    options.searchMethod = options.searchMethod || WalkerSearch.SEARCH_METHOD_CONTAINS;
+    options.types = options.types || WalkerSearch.ALL_RESULTS_TYPES;
+
+    // Empty strings will return no results, as will non-string input
+    if (typeof query !== "string") {
+      query = "";
+    }
+
+    // Store results in a map indexed by nodes to avoid duplicate results
+    let results = new Map();
+
+    // Search through the indexed data
+    this._searchIndex(query, options, results);
+
+    // Search with querySelectorAll
+    this._searchSelectors(query, options, results);
+
+    // Concatenate all results into an Array to return
+    let resultList = [];
+    for (let [node, matches] of results) {
+      for (let {type} of matches) {
+        resultList.push({
+          node: node,
+          type: type,
+        });
+
+        // For now, just do one result per node since the frontend
+        // doesn't have a way to highlight each result individually
+        // yet.
+        break;
+      }
+    }
+
+    let documents = this.walker.tabActor.windows.map(win=>win.document);
+
+    // Sort the resulting nodes by order of appearance in the DOM
+    resultList.sort((a,b) => {
+      // Disconnected nodes won't get good results from compareDocumentPosition
+      // so check the order of their document instead.
+      if (a.node.ownerDocument != b.node.ownerDocument) {
+        let indA = documents.indexOf(a.node.ownerDocument);
+        let indB = documents.indexOf(b.node.ownerDocument);
+        return indA - indB;
+      }
+      // If the same document, then sort on DOCUMENT_POSITION_FOLLOWING (4)
+      // which means B is after A.
+      return a.node.compareDocumentPosition(b.node) & 4 ? -1 : 1;
+    });
+
+    return resultList;
+  }
+};
+
+WalkerSearch.SEARCH_METHOD_CONTAINS = (query, candidate) => {
+  return query && candidate.toLowerCase().indexOf(query.toLowerCase()) !== -1;
+};
+
+WalkerSearch.ALL_RESULTS_TYPES = ["tag", "text", "attributeName",
+                                  "attributeValue", "selector"];
+
+exports.WalkerSearch = WalkerSearch;
--- a/devtools/server/tests/mochitest/chrome.ini
+++ b/devtools/server/tests/mochitest/chrome.ini
@@ -5,16 +5,17 @@ support-files =
   Debugger.Source.prototype.element.js
   Debugger.Source.prototype.element-2.js
   Debugger.Source.prototype.element.html
   director-helpers.js
   hello-actor.js
   inspector_getImageData.html
   inspector-delay-image-response.sjs
   inspector-helpers.js
+  inspector-search-data.html
   inspector-styles-data.css
   inspector-styles-data.html
   inspector-traversal-data.html
   large-image.jpg
   memory-helpers.js
   memprof-helpers.js
   nonchrome_unsafeDereference.html
   small-image.gif
@@ -72,16 +73,18 @@ skip-if = buildapp == 'mulet'
 [test_inspector-mutations-frameload.html]
 [test_inspector-mutations-value.html]
 [test_inspector-pseudoclass-lock.html]
 [test_inspector-release.html]
 [test_inspector-reload.html]
 [test_inspector-remove.html]
 [test_inspector-resolve-url.html]
 [test_inspector-retain.html]
+[test_inspector-search.html]
+[test_inspector-search-front.html]
 [test_inspector-scroll-into-view.html]
 [test_inspector-traversal.html]
 [test_makeGlobalObjectReference.html]
 [test_memory.html]
 [test_memory_allocations_01.html]
 [test_memory_allocations_02.html]
 [test_memory_allocations_03.html]
 [test_memory_allocations_04.html]
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/mochitest/inspector-search-data.html
@@ -0,0 +1,52 @@
+<html>
+<head>
+  <meta charset="UTF-8">
+  <title>Inspector Search Test Data</title>
+  <style>
+    #pseudo {
+      display: block;
+      margin: 0;
+    }
+    #pseudo:before {
+      content: "before element";
+    }
+    #pseudo:after {
+      content: "after element";
+    }
+  </style>
+  <script type="text/javascript">
+    window.onload = function() {
+      window.opener.postMessage('ready', '*');
+    };
+  </script>
+</head>
+</body>
+  <!-- A comment
+       spread across multiple lines -->
+
+  <img width="100" height="100" src="large-image.jpg" />
+
+  <h1 id="pseudo">Heading 1</h1>
+  <p>A p tag with the text 'h1' inside of it.
+    <strong>A strong h1 result</strong>
+  </p>
+
+  <div id="arrows" northwest="↖" northeast="↗" southeast="↘" southwest="↙">
+    Unicode arrows
+  </div>
+
+  <h2>Heading 2</h2>
+  <h2>Heading 2</h2>
+  <h2>Heading 2</h2>
+
+  <h3>Heading 3</h3>
+  <h3>Heading 3</h3>
+  <h3>Heading 3</h3>
+
+  <h4>Heading 4</h4>
+  <h4>Heading 4</h4>
+  <h4>Heading 4</h4>
+
+  <div class="💩" id="💩" 💩="💩"></div>
+</body>
+</html>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/mochitest/test_inspector-search-front.html
@@ -0,0 +1,220 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=835896
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 835896</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+  <script type="application/javascript;version=1.8" src="inspector-helpers.js"></script>
+  <script type="application/javascript;version=1.8">
+window.onload = function() {
+  const Cu = Components.utils;
+  Cu.import("resource://gre/modules/devtools/Loader.jsm");
+  const {Promise: promise} =
+    Cu.import("resource://gre/modules/Promise.jsm", {});
+  const {InspectorFront} =
+    devtools.require("devtools/server/actors/inspector");
+  const {console} =
+    Cu.import("resource://gre/modules/devtools/shared/Console.jsm", {});
+
+  SimpleTest.waitForExplicitFinish();
+
+  let walkerFront = null;
+  let inspectee = null;
+  let inspector = null;
+
+  // WalkerFront specific tests.  These aren't to excercise search
+  // edge cases so much as to test the state the Front maintains between
+  // searches.
+  // See also test_inspector-search.html
+
+  addAsyncTest(function* setup() {
+    info ("Setting up inspector and walker actors.");
+
+    let url = document.getElementById("inspectorContent").href;
+
+    yield new Promise(resolve => {
+      attachURL(url, function(err, client, tab, doc) {
+        inspectee = doc;
+        inspector = InspectorFront(client, tab);
+        resolve();
+      });
+    });
+
+    walkerFront = yield inspector.getWalker();
+    ok(walkerFront, "getWalker() should return an actor.");
+
+    runNextTest();
+  });
+
+  addAsyncTest(function* testWalkerFrontDefaults() {
+    info ("Testing search API using WalkerFront.");
+    let nodes = yield walkerFront.querySelectorAll(walkerFront.rootNode, "h2");
+    let fronts = yield nodes.items();
+
+    let frontResult = yield walkerFront.search("");
+    ok(!frontResult, "Null result on front when searching for ''");
+
+    let results = yield walkerFront.search("h2");
+    isDeeply(results, {
+      node: fronts[0],
+      type: "search",
+      resultsIndex: 0,
+      resultsLength: 3
+    }, "Default options work");
+
+    results = yield walkerFront.search("h2", { });
+    isDeeply(results, {
+      node: fronts[1],
+      type: "search",
+      resultsIndex: 1,
+      resultsLength: 3
+    }, "Search works with empty options");
+
+    // Clear search data to remove result state on the front
+    yield walkerFront.search("");
+    runNextTest();
+  });
+
+  addAsyncTest(function* testMultipleSearches() {
+    info ("Testing search API using WalkerFront (reverse=false)");
+    let nodes = yield walkerFront.querySelectorAll(walkerFront.rootNode, "h2");
+    let fronts = yield nodes.items();
+
+    let results = yield walkerFront.search("h2");
+    isDeeply(results, {
+      node: fronts[0],
+      type: "search",
+      resultsIndex: 0,
+      resultsLength: 3
+    }, "Search works with multiple results (reverse=false)");
+
+    results = yield walkerFront.search("h2");
+    isDeeply(results, {
+      node: fronts[1],
+      type: "search",
+      resultsIndex: 1,
+      resultsLength: 3
+    }, "Search works with multiple results (reverse=false)");
+
+    results = yield walkerFront.search("h2");
+    isDeeply(results, {
+      node: fronts[2],
+      type: "search",
+      resultsIndex: 2,
+      resultsLength: 3
+    }, "Search works with multiple results (reverse=false)");
+
+    results = yield walkerFront.search("h2");
+    isDeeply(results, {
+      node: fronts[0],
+      type: "search",
+      resultsIndex: 0,
+      resultsLength: 3
+    }, "Search works with multiple results (reverse=false)");
+
+    // Clear search data to remove result state on the front
+    yield walkerFront.search("");
+    runNextTest();
+  });
+
+  addAsyncTest(function* testMultipleSearchesReverse() {
+    info ("Testing search API using WalkerFront (reverse=true)");
+    let nodes = yield walkerFront.querySelectorAll(walkerFront.rootNode, "h2");
+    let fronts = yield nodes.items();
+
+    let results = yield walkerFront.search("h2", {reverse: true});
+    isDeeply(results, {
+      node: fronts[2],
+      type: "search",
+      resultsIndex: 2,
+      resultsLength: 3
+    }, "Search works with multiple results (reverse=true)");
+
+    results = yield walkerFront.search("h2", {reverse: true});
+    isDeeply(results, {
+      node: fronts[1],
+      type: "search",
+      resultsIndex: 1,
+      resultsLength: 3
+    }, "Search works with multiple results (reverse=true)");
+
+    results = yield walkerFront.search("h2", {reverse: true});
+    isDeeply(results, {
+      node: fronts[0],
+      type: "search",
+      resultsIndex: 0,
+      resultsLength: 3
+    }, "Search works with multiple results (reverse=true)");
+
+    results = yield walkerFront.search("h2", {reverse: true});
+    isDeeply(results, {
+      node: fronts[2],
+      type: "search",
+      resultsIndex: 2,
+      resultsLength: 3
+    }, "Search works with multiple results (reverse=true)");
+
+    results = yield walkerFront.search("h2", {reverse: false});
+    isDeeply(results, {
+      node: fronts[0],
+      type: "search",
+      resultsIndex: 0,
+      resultsLength: 3
+    }, "Search works with multiple results (reverse=false)");
+
+    // Clear search data to remove result state on the front
+    yield walkerFront.search("");
+    runNextTest();
+  });
+
+
+  addAsyncTest(function* testBackwardsCompat() {
+    info ("Simulating a server that doesn't have the new search functionality.");
+    walkerFront.traits.textSearch = false;
+    let front = yield walkerFront.querySelector(walkerFront.rootNode, "h1");
+
+    let results = yield walkerFront.search("h1");
+    isDeeply(results, {
+      node: front,
+      type: "selector",
+      resultsIndex: 0,
+      resultsLength: 1
+    }, "Only querySelectorAll results being returned");
+
+    // Clear search data to remove result state on the front
+    yield walkerFront.search("");
+
+    // Reset the normal textSearch behavior
+    walkerFront.traits.textSearch = true;
+
+    results = yield walkerFront.search("h1");
+    isDeeply(results, {
+      node: front,
+      type: "search",
+      resultsIndex: 0,
+      resultsLength: 3
+    }, "Other results being included");
+
+    // Clear search data to remove result state on the front
+    yield walkerFront.search("");
+    runNextTest();
+  });
+
+  runNextTest();
+};
+  </script>
+</head>
+<body>
+<a id="inspectorContent" target="_blank" href="inspector-search-data.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/mochitest/test_inspector-search.html
@@ -0,0 +1,300 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=835896
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 835896</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+  <script type="application/javascript;version=1.8" src="inspector-helpers.js"></script>
+  <script type="application/javascript;version=1.8">
+window.onload = function() {
+  const Cu = Components.utils;
+  Cu.import("resource://gre/modules/devtools/Loader.jsm");
+  const {Promise: promise} =
+    Cu.import("resource://gre/modules/Promise.jsm", {});
+  const {InspectorFront} =
+    devtools.require("devtools/server/actors/inspector");
+  const {WalkerSearch, WalkerIndex} =
+    devtools.require("devtools/server/actors/utils/walker-search");
+  const {console} =
+    Cu.import("resource://gre/modules/devtools/shared/Console.jsm", {});
+
+  SimpleTest.waitForExplicitFinish();
+
+  let walkerActor = null;
+  let walkerSearch = null;
+  let inspectee = null;
+  let inspector = null;
+
+  // WalkerSearch specific tests.  This is to make sure search results are
+  // coming back as expected.
+  // See also test_inspector-search-front.html.
+
+  addAsyncTest(function* setup() {
+    info ("Setting up inspector and walker actors.");
+
+    let url = document.getElementById("inspectorContent").href;
+
+    yield new Promise(resolve => {
+      attachURL(url, function(err, client, tab, doc) {
+        inspectee = doc;
+        inspector = InspectorFront(client, tab);
+        resolve();
+      });
+    });
+
+    let walkerFront = yield inspector.getWalker();
+    ok(walkerFront, "getWalker() should return an actor.");
+
+    let serverConnection = walkerFront.conn._transport._serverConnection;
+    walkerActor = serverConnection.getActor(walkerFront.actorID);
+    ok(walkerActor,
+      "Got a reference to the walker actor (" + walkerFront.actorID + ")");
+
+    walkerSearch = walkerActor.walkerSearch;
+
+    runNextTest();
+  });
+
+  addAsyncTest(function* testIndexExists() {
+    info ("Testing basic index APIs exist.");
+
+    let index = new WalkerIndex(walkerActor);
+    ok(index.data.size > 0, "public index is filled after getting");
+
+    index.clearIndex();
+    ok(!index._data, "private index is empty after clearing");
+    ok(index.data.size > 0, "public index is filled after getting");
+
+    index.destroy();
+    runNextTest();
+  });
+
+  addAsyncTest(function* testSearchExists() {
+    info ("Testing basic search APIs exist.");
+
+    ok(walkerSearch, "walker search exists on the WalkerActor");
+    ok(walkerSearch.search, "walker search has `search` method");
+    ok(walkerSearch.index, "walker search has `index` property");
+    is(walkerSearch.walker, walkerActor, "referencing the correct WalkerActor");
+
+    let search = new WalkerSearch(walkerActor);
+    ok(search, "a new search instance can be created");
+    ok(search.search, "new search instance has `search` method");
+    ok(search.index, "new search instance has `index` property");
+    isnot(search, walkerSearch, "new search instance differs from the WalkerActor's");
+
+    search.destroy();
+    runNextTest();
+  });
+
+  addAsyncTest(function* testEmptySearch() {
+    info ("Testing search with an empty query.");
+    results = walkerSearch.search("");
+    is(results.length, 0, "No results when searching for ''");
+
+    results = walkerSearch.search(null);
+    is(results.length, 0, "No results when searching for null");
+
+    results = walkerSearch.search(undefined);
+    is(results.length, 0, "No results when searching for undefined");
+
+    results = walkerSearch.search(10);
+    is(results.length, 0, "No results when searching for 10");
+
+    runNextTest();
+  });
+
+  addAsyncTest(function* testBasicSearchData() {
+    let testData = [
+    {
+      desc: "Search for tag with one result.",
+      search: "body",
+      expected: [
+        {node: inspectee.body, type: "tag"}
+      ]
+    },
+    {
+      desc: "Search for tag with multiple results",
+      search: "h2",
+      expected: [
+        {node: inspectee.querySelectorAll("h2")[0], type: "tag"},
+        {node: inspectee.querySelectorAll("h2")[1], type: "tag"},
+        {node: inspectee.querySelectorAll("h2")[2], type: "tag"},
+      ]
+    },
+    {
+      desc: "Search for selector with multiple results",
+      search: "body > h2",
+      expected: [
+        {node: inspectee.querySelectorAll("h2")[0], type: "selector"},
+        {node: inspectee.querySelectorAll("h2")[1], type: "selector"},
+        {node: inspectee.querySelectorAll("h2")[2], type: "selector"},
+      ]
+    },
+    {
+      desc: "Search for selector with multiple results",
+      search: ":root h2",
+      expected: [
+        {node: inspectee.querySelectorAll("h2")[0], type: "selector"},
+        {node: inspectee.querySelectorAll("h2")[1], type: "selector"},
+        {node: inspectee.querySelectorAll("h2")[2], type: "selector"},
+      ]
+    },
+    {
+      desc: "Search for selector with multiple results",
+      search: "* h2",
+      expected: [
+        {node: inspectee.querySelectorAll("h2")[0], type: "selector"},
+        {node: inspectee.querySelectorAll("h2")[1], type: "selector"},
+        {node: inspectee.querySelectorAll("h2")[2], type: "selector"},
+      ]
+    },
+    {
+      desc: "Search with multiple matches in a single tag expecting a single result",
+      search: "💩",
+      expected: [
+        {node: inspectee.getElementById("💩"), type: "attributeName"}
+      ]
+    },
+    {
+      desc: "Search that has tag and text results",
+      search: "h1",
+      expected: [
+        {node: inspectee.querySelector("h1"), type: "tag"},
+        {node: inspectee.querySelector("h1 + p").childNodes[0], type: "text"},
+        {node: inspectee.querySelector("h1 + p > strong").childNodes[0], type: "text"},
+      ]
+    },
+    ]
+
+    for (let {desc, search, expected} of testData) {
+      info("Running test: " + desc);
+      let results = walkerSearch.search(search);
+      isDeeply(results, expected,
+        "Search returns correct results with '" + search + "'");
+    }
+
+    runNextTest();
+  });
+
+  addAsyncTest(function* testPseudoElements() {
+    info ("Testing ::before and ::after element matching");
+
+    let beforeElt = new _documentWalker(inspectee.querySelector("#pseudo"),
+                                        inspectee.defaultView).firstChild();
+    let afterElt = new _documentWalker(inspectee.querySelector("#pseudo"),
+                                       inspectee.defaultView).lastChild();
+    let styleText = inspectee.querySelector("style").childNodes[0];
+
+    // ::before
+    let results = walkerSearch.search("::before");
+    isDeeply(results, [ {node: beforeElt, type: "tag"} ],
+             "Tag search works for pseudo element");
+
+    results = walkerSearch.search("_moz_generated_content_before");
+    is(results.length, 0, "No results for anon tag name");
+
+    results = walkerSearch.search("before element");
+    isDeeply(results, [
+      {node: styleText, type: "text"},
+      {node: beforeElt, type: "text"}
+    ], "Text search works for pseudo element");
+
+    // ::after
+    results = walkerSearch.search("::after");
+    isDeeply(results, [ {node: afterElt, type: "tag"} ],
+             "Tag search works for pseudo element");
+
+    results = walkerSearch.search("_moz_generated_content_after");
+    is(results.length, 0, "No results for anon tag name");
+
+    results = walkerSearch.search("after element");
+    isDeeply(results, [
+      {node: styleText, type: "text"},
+      {node: afterElt, type: "text"}
+    ], "Text search works for pseudo element");
+
+    runNextTest();
+  });
+
+  addAsyncTest(function* testSearchMutationChangeResults() {
+    info ("Testing search before and after a mutation.");
+    let expected = [
+      {node: inspectee.querySelectorAll("h3")[0], type: "tag"},
+      {node: inspectee.querySelectorAll("h3")[1], type: "tag"},
+      {node: inspectee.querySelectorAll("h3")[2], type: "tag"},
+    ];
+
+    let results = walkerSearch.search("h3");
+    isDeeply(results, expected, "Search works with tag results");
+
+    yield mutateDocumentAndWaitForMutation(() => {
+      expected[0].node.remove();
+    });
+
+    results = walkerSearch.search("h3");
+    isDeeply(results, [
+      expected[1],
+      expected[2]
+    ], "Results are updated after removal");
+
+    yield new Promise(resolve => {
+      info("Waiting for a mutation to happen");
+      let observer = new inspectee.defaultView.MutationObserver(() => {
+        resolve();
+      });
+      observer.observe(inspectee, {attributes: true, subtree: true});
+      inspectee.body.setAttribute("h3", "true");
+    });
+
+    results = walkerSearch.search("h3");
+    isDeeply(results, [
+      {node: inspectee.body, type: "attributeName"},
+      expected[1],
+      expected[2]
+    ], "Results are updated after addition");
+
+    yield new Promise(resolve => {
+      info("Waiting for a mutation to happen");
+      let observer = new inspectee.defaultView.MutationObserver(() => {
+        resolve();
+      });
+      observer.observe(inspectee, {attributes: true, childList: true, subtree: true});
+      inspectee.body.removeAttribute("h3");
+      expected[1].node.remove();
+      expected[2].node.remove();
+    });
+
+    results = walkerSearch.search("h3");
+    is(results.length, 0, "Results are updated after removal");
+
+    runNextTest();
+  });
+
+  runNextTest();
+
+  function mutateDocumentAndWaitForMutation(mutationFn) {
+    return new Promise(resolve => {
+      info("Listening to markup mutation on the inspectee");
+      let observer = new inspectee.defaultView.MutationObserver(resolve);
+      observer.observe(inspectee, {childList: true, subtree: true});
+      mutationFn();
+    });
+  }
+};
+  </script>
+</head>
+<body>
+<a id="inspectorContent" target="_blank" href="inspector-search-data.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
--- a/devtools/server/tests/unit/test_getRuleText.js
+++ b/devtools/server/tests/unit/test_getRuleText.js
@@ -95,16 +95,23 @@ const TEST_DATA = [
   },
   {
     desc: "Content string containing a } character",
     input: "   #id{border:1px solid red;content: '}';color:red;}",
     line: 1,
     column: 4,
     expected: {offset: 7, text: "border:1px solid red;content: '}';color:red;"}
   },
+  {
+    desc: "Rule contains no tokens",
+    input: "div{}",
+    line: 1,
+    column: 1,
+    expected: {offset: 4, text: ""}
+  },
 ];
 
 function run_test() {
   for (let test of TEST_DATA) {
     do_print("Starting test: " + test.desc);
     do_print("Input string " + test.input);
     let output;
     try {
--- a/devtools/shared/heapsnapshot/DeserializedNode.cpp
+++ b/devtools/shared/heapsnapshot/DeserializedNode.cpp
@@ -70,55 +70,41 @@ Concrete<DeserializedNode>::typeName() c
 Node::Size
 Concrete<DeserializedNode>::size(mozilla::MallocSizeOf mallocSizeof) const
 {
   return get().size;
 }
 
 class DeserializedEdgeRange : public EdgeRange
 {
-  EdgeVector edges;
-  size_t     i;
+  DeserializedNode* node;
+  Edge              currentEdge;
+  size_t            i;
 
   void settle() {
-    front_ = i < edges.length() ? &edges[i] : nullptr;
+    if (i >= node->edges.length()) {
+      front_ = nullptr;
+      return;
+    }
+
+    auto& edge = node->edges[i];
+    auto referent = node->getEdgeReferent(edge);
+    currentEdge = mozilla::Move(Edge(edge.name ? NS_strdup(edge.name) : nullptr,
+                                     referent));
+    front_ = &currentEdge;
   }
 
 public:
-  explicit DeserializedEdgeRange()
-    : edges()
+  explicit DeserializedEdgeRange(DeserializedNode& node)
+    : node(&node)
     , i(0)
   {
     settle();
   }
 
-  bool init(DeserializedNode& node)
-  {
-    if (!edges.reserve(node.edges.length()))
-      return false;
-
-    for (DeserializedEdge* edgep = node.edges.begin();
-         edgep != node.edges.end();
-         edgep++)
-    {
-      char16_t* name = nullptr;
-      if (edgep->name) {
-        name = NS_strdup(edgep->name);
-        if (!name)
-          return false;
-      }
-
-      auto referent = node.getEdgeReferent(*edgep);
-      edges.infallibleAppend(mozilla::Move(Edge(name, referent)));
-    }
-
-    settle();
-    return true;
-  }
-
   void popFront() override
   {
     i++;
     settle();
   }
 };
 
 StackFrame
@@ -133,19 +119,19 @@ Concrete<DeserializedNode>::allocationSt
   return JS::ubi::StackFrame(const_cast<DeserializedStackFrame*>(&*ptr));
 }
 
 
 UniquePtr<EdgeRange>
 Concrete<DeserializedNode>::edges(JSRuntime* rt, bool) const
 {
   UniquePtr<DeserializedEdgeRange, JS::DeletePolicy<DeserializedEdgeRange>> range(
-    js_new<DeserializedEdgeRange>());
+    js_new<DeserializedEdgeRange>(get()));
 
-  if (!range || !range->init(get()))
+  if (!range)
     return nullptr;
 
   return UniquePtr<EdgeRange>(range.release());
 }
 
 StackFrame
 ConcreteStackFrame<DeserializedStackFrame>::parent() const
 {
--- a/devtools/shared/heapsnapshot/tests/gtest/DeserializedNodeUbiNodes.cpp
+++ b/devtools/shared/heapsnapshot/tests/gtest/DeserializedNodeUbiNodes.cpp
@@ -63,38 +63,38 @@ DEF_TEST(DeserializedNodeUbiNodes, {
 
     // Test the ubi::Node's edges.
 
     UniquePtr<DeserializedNode> referent1(new MockDeserializedNode(1,
                                                                    nullptr,
                                                                    10));
     DeserializedEdge edge1(referent1->id);
     mocked.addEdge(Move(edge1));
-    EXPECT_CALL(mocked,
-                getEdgeReferent(Field(&DeserializedEdge::referent,
-                                      referent1->id)))
+    EXPECT_CALL(mocked, getEdgeReferent(EdgeTo(referent1->id)))
       .Times(1)
       .WillOnce(Return(JS::ubi::Node(referent1.get())));
 
     UniquePtr<DeserializedNode> referent2(new MockDeserializedNode(2,
                                                                    nullptr,
                                                                    20));
     DeserializedEdge edge2(referent2->id);
     mocked.addEdge(Move(edge2));
-    EXPECT_CALL(mocked,
-                getEdgeReferent(Field(&DeserializedEdge::referent,
-                                      referent2->id)))
+    EXPECT_CALL(mocked, getEdgeReferent(EdgeTo(referent2->id)))
       .Times(1)
       .WillOnce(Return(JS::ubi::Node(referent2.get())));
 
     UniquePtr<DeserializedNode> referent3(new MockDeserializedNode(3,
                                                                    nullptr,
                                                                    30));
     DeserializedEdge edge3(referent3->id);
     mocked.addEdge(Move(edge3));
-    EXPECT_CALL(mocked,
-                getEdgeReferent(Field(&DeserializedEdge::referent,
-                                      referent3->id)))
+    EXPECT_CALL(mocked, getEdgeReferent(EdgeTo(referent3->id)))
       .Times(1)
       .WillOnce(Return(JS::ubi::Node(referent3.get())));
 
-    ubi.edges(rt);
+    auto range = ubi.edges(rt);
+    ASSERT_TRUE(!!range);
+
+    for ( ; !range->empty(); range->popFront()) {
+      // Nothing to do here. This loop ensures that we get each edge referent
+      // that we expect above.
+    }
   });
--- a/devtools/shared/heapsnapshot/tests/gtest/DevTools.h
+++ b/devtools/shared/heapsnapshot/tests/gtest/DevTools.h
@@ -273,16 +273,22 @@ MATCHER_P(UTF16StrEq, str, "") {
 MATCHER_P(UniqueUTF16StrEq, str, "") {
   return NS_strcmp(arg.get(), str) == 0;
 }
 
 MATCHER(UniqueIsNull, "") {
   return arg.get() == nullptr;
 }
 
+// Matches an edge whose referent is the node with the given id.
+MATCHER_P(EdgeTo, id, "") {
+  return Matcher<const DeserializedEdge&>(Field(&DeserializedEdge::referent, id))
+    .MatchAndExplain(arg, result_listener);
+}
+
 } // namespace testing
 
 
 // A mock `Writer` class to be used with testing `WriteHeapGraph`.
 class MockWriter : public CoreDumpWriter
 {
 public:
   virtual ~MockWriter() override { }
--- a/dom/interfaces/security/nsIContentSecurityManager.idl
+++ b/dom/interfaces/security/nsIContentSecurityManager.idl
@@ -41,14 +41,14 @@ interface nsIContentSecurityManager : ns
    nsIStreamListener performSecurityCheck(in nsIChannel aChannel,
                                           in nsIStreamListener aStreamListener);
 
   /**
    * Implementation of
    * https://w3c.github.io/webappsec-secure-contexts/#is-origin-trustworthy
    *
    * This method should only be used when the context of the URI isn't available
-   * since isSecureContext is preffered as it handles parent contexts.
+   * since isSecureContext is preferred as it handles parent contexts.
    *
    * This method returns false instead of throwing upon errors.
    */
   boolean isURIPotentiallyTrustworthy(in nsIURI aURI);
 };
--- a/dom/media/mediasource/MediaSourceDecoder.cpp
+++ b/dom/media/mediasource/MediaSourceDecoder.cpp
@@ -80,17 +80,18 @@ MediaSourceDecoder::GetSeekable()
 
   media::TimeIntervals seekable;
   double duration = mMediaSource->Duration();
   if (IsNaN(duration)) {
     // Return empty range.
   } else if (duration > 0 && mozilla::IsInfinite(duration)) {
     media::TimeIntervals buffered = GetBuffered();
     if (buffered.Length()) {
-      seekable += media::TimeInterval(buffered.GetStart(), buffered.GetEnd());
+      seekable +=
+        media::TimeInterval(media::TimeUnit::FromSeconds(0), buffered.GetEnd());
     }
   } else {
     seekable += media::TimeInterval(media::TimeUnit::FromSeconds(0),
                                     media::TimeUnit::FromSeconds(duration));
   }
   MSE_DEBUG("ranges=%s", DumpTimeRanges(seekable).get());
   return seekable;
 }
--- a/dom/media/webaudio/AudioContext.cpp
+++ b/dom/media/webaudio/AudioContext.cpp
@@ -522,18 +522,17 @@ already_AddRefed<PeriodicWave>
 AudioContext::CreatePeriodicWave(const Float32Array& aRealData,
                                  const Float32Array& aImagData,
                                  ErrorResult& aRv)
 {
   aRealData.ComputeLengthAndData();
   aImagData.ComputeLengthAndData();
 
   if (aRealData.Length() != aImagData.Length() ||
-      aRealData.Length() == 0 ||
-      aRealData.Length() > 4096) {
+      aRealData.Length() == 0) {
     aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
     return nullptr;
   }
 
   RefPtr<PeriodicWave> periodicWave =
     new PeriodicWave(this, aRealData.Data(), aImagData.Data(),
                      aImagData.Length(), aRv);
   if (aRv.Failed()) {
--- a/dom/media/webaudio/PeriodicWave.cpp
+++ b/dom/media/webaudio/PeriodicWave.cpp
@@ -22,17 +22,16 @@ PeriodicWave::PeriodicWave(AudioContext*
                            const uint32_t aLength,
                            ErrorResult& aRv)
   : mContext(aContext)
 {
   MOZ_ASSERT(aContext);
 
   // Caller should have checked this and thrown.
   MOZ_ASSERT(aLength > 0);
-  MOZ_ASSERT(aLength <= 4096);
   mLength = aLength;
 
   // Copy coefficient data. The two arrays share an allocation.
   mCoefficients = new ThreadSharedFloatArrayBufferList(2);
   float* buffer = static_cast<float*>(malloc(aLength*sizeof(float)*2));
   if (buffer == nullptr) {
     aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
     return;
--- a/dom/media/webaudio/blink/PeriodicWave.cpp
+++ b/dom/media/webaudio/blink/PeriodicWave.cpp
@@ -26,86 +26,92 @@
  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
 #include "PeriodicWave.h"
 #include <algorithm>
 #include <cmath>
 #include "mozilla/FFTBlock.h"
 
-const unsigned PeriodicWaveSize = 4096; // This must be a power of two.
-const unsigned NumberOfRanges = 36; // There should be 3 * log2(PeriodicWaveSize) 1/3 octave ranges.
+const unsigned MinPeriodicWaveSize = 4096; // This must be a power of two.
+const unsigned MaxPeriodicWaveSize = 8192; // This must be a power of two.
 const float CentsPerRange = 1200 / 3; // 1/3 Octave.
 
 using namespace mozilla;
 using mozilla::dom::OscillatorType;
 
 namespace WebCore {
 
 already_AddRefed<PeriodicWave>
 PeriodicWave::create(float sampleRate,
                      const float* real,
                      const float* imag,
                      size_t numberOfComponents)
 {
-    bool isGood = real && imag && numberOfComponents > 0 &&
-         numberOfComponents <= PeriodicWaveSize;
+    bool isGood = real && imag && numberOfComponents > 0;
     MOZ_ASSERT(isGood);
     if (isGood) {
         RefPtr<PeriodicWave> periodicWave =
-            new PeriodicWave(sampleRate);
+            new PeriodicWave(sampleRate, numberOfComponents);
         periodicWave->createBandLimitedTables(real, imag, numberOfComponents);
         return periodicWave.forget();
     }
     return nullptr;
 }
 
 already_AddRefed<PeriodicWave>
 PeriodicWave::createSine(float sampleRate)
 {
     RefPtr<PeriodicWave> periodicWave =
-        new PeriodicWave(sampleRate);
+        new PeriodicWave(sampleRate, MinPeriodicWaveSize);
     periodicWave->generateBasicWaveform(OscillatorType::Sine);
     return periodicWave.forget();
 }
 
 already_AddRefed<PeriodicWave>
 PeriodicWave::createSquare(float sampleRate)
 {
     RefPtr<PeriodicWave> periodicWave =
-        new PeriodicWave(sampleRate);
+        new PeriodicWave(sampleRate, MinPeriodicWaveSize);
     periodicWave->generateBasicWaveform(OscillatorType::Square);
     return periodicWave.forget();
 }
 
 already_AddRefed<PeriodicWave>
 PeriodicWave::createSawtooth(float sampleRate)
 {
     RefPtr<PeriodicWave> periodicWave =
-        new PeriodicWave(sampleRate);
+        new PeriodicWave(sampleRate, MinPeriodicWaveSize);
     periodicWave->generateBasicWaveform(OscillatorType::Sawtooth);
     return periodicWave.forget();
 }
 
 already_AddRefed<PeriodicWave>
 PeriodicWave::createTriangle(float sampleRate)
 {
     RefPtr<PeriodicWave> periodicWave =
-        new PeriodicWave(sampleRate);
+        new PeriodicWave(sampleRate, MinPeriodicWaveSize);
     periodicWave->generateBasicWaveform(OscillatorType::Triangle);
     return periodicWave.forget();
 }
 
-PeriodicWave::PeriodicWave(float sampleRate)
+PeriodicWave::PeriodicWave(float sampleRate, size_t numberOfComponents)
     : m_sampleRate(sampleRate)
-    , m_periodicWaveSize(PeriodicWaveSize)
-    , m_numberOfRanges(NumberOfRanges)
     , m_centsPerRange(CentsPerRange)
 {
     float nyquist = 0.5 * m_sampleRate;
+
+    if (numberOfComponents <= MinPeriodicWaveSize) {
+        m_periodicWaveSize = MinPeriodicWaveSize;
+    } else {
+        unsigned npow2 = powf(2.0f, floorf(logf(numberOfComponents - 1.0)/logf(2.0f) + 1.0f));
+        m_periodicWaveSize = std::min(MaxPeriodicWaveSize, npow2);
+    }
+
+    m_numberOfRanges = (unsigned)(3.0f*logf(m_periodicWaveSize)/logf(2.0f));
     m_lowestFundamentalFrequency = nyquist / maxNumberOfPartials();
     m_rateScale = m_periodicWaveSize / m_sampleRate;
 }
 
 size_t PeriodicWave::sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const
 {
     size_t amount = aMallocSizeOf(this);
 
--- a/dom/media/webaudio/blink/PeriodicWave.h
+++ b/dom/media/webaudio/blink/PeriodicWave.h
@@ -71,17 +71,17 @@ public:
     float rateScale() const { return m_rateScale; }
 
     unsigned periodicWaveSize() const { return m_periodicWaveSize; }
     float sampleRate() const { return m_sampleRate; }
 
     size_t sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
 
 private:
-    explicit PeriodicWave(float sampleRate);
+    explicit PeriodicWave(float sampleRate, size_t numberOfComponents);
     ~PeriodicWave() {}
 
     void generateBasicWaveform(mozilla::dom::OscillatorType);
 
     float m_sampleRate;
     unsigned m_periodicWaveSize;
     unsigned m_numberOfRanges;
     float m_centsPerRange;
--- a/dom/media/webaudio/test/test_periodicWave.html
+++ b/dom/media/webaudio/test/test_periodicWave.html
@@ -32,19 +32,19 @@ addLoadEvent(function() {
   var ac = new AudioContext();
   ac.createPeriodicWave(new Float32Array(4096), new Float32Array(4096));
   expectException(function() {
     ac.createPeriodicWave(new Float32Array(512), imag);
   }, DOMException.NOT_SUPPORTED_ERR);
   expectException(function() {
     ac.createPeriodicWave(new Float32Array(0), new Float32Array(0));
   }, DOMException.NOT_SUPPORTED_ERR);
-  expectException(function() {
+  expectNoException(function() {
     ac.createPeriodicWave(new Float32Array(4097), new Float32Array(4097));
-  }, DOMException.NOT_SUPPORTED_ERR);
+  });
 
   runTest();
 });
 
 var gTest = {
   createGraph: function(context) {
     var merger = context.createChannelMerger();
 
--- a/dom/media/webrtc/MediaEngineDefault.h
+++ b/dom/media/webrtc/MediaEngineDefault.h
@@ -131,17 +131,18 @@ public:
                           bool aNoiseOn, uint32_t aNoise,
                           int32_t aPlayoutDelay) override { return NS_OK; };
   void AppendToSegment(AudioSegment& aSegment, TrackTicks aSamples);
   virtual void NotifyPull(MediaStreamGraph* aGraph,
                           SourceMediaStream *aSource,
                           TrackID aId,
                           StreamTime aDesiredTime) override
   {
-    NS_WARN_IF_FALSE(aDesiredTime <= aSource->GetEndOfAppendedData(aId),
+    NS_WARN_IF_FALSE(!aSource->FindTrack(aId) ||
+                     aDesiredTime <= aSource->GetEndOfAppendedData(aId),
                      "MediaEngineDefaultAudioSource data underrun");
   }
 
   virtual bool IsFake() override {
     return true;
   }
 
   virtual const dom::MediaSourceEnum GetMediaSource() override {
--- a/dom/tests/browser/browser.ini
+++ b/dom/tests/browser/browser.ini
@@ -3,16 +3,19 @@ skip-if = e10s # Bug ?????? - most of th
 support-files =
   browser_frame_elements.html
   page_privatestorageevent.html
   position.html
   test-console-api.html
   test_bug1004814.html
   worker_bug1004814.js
 
+[browser_test_toolbars_visibility.js]
+support-files =
+  test_new_window_from_content_child.html
 [browser_bug1008941_dismissGeolocationHanger.js]
 skip-if = buildapp == 'mulet'
 [browser_test__content.js]
 [browser_ConsoleAPITests.js]
 [browser_ConsoleStorageAPITests.js]
 [browser_ConsoleStoragePBTest_perwindowpb.js]
 [browser_autofocus_background.js]
 skip-if= buildapp == 'mulet'
new file mode 100644
--- /dev/null
+++ b/dom/tests/browser/browser_test_toolbars_visibility.js
@@ -0,0 +1,187 @@
+// Tests that toolbars have proper visibility when opening a new window
+// in either content or chrome context.
+
+const CONTENT_PAGE = "http://www.example.com/browser/dom/tests/browser/test_new_window_from_content_child.html";
+
+/**
+ * This function retrieves the visibility state of the toolbars of a
+ * window within the content context.
+ *
+ * @param aBrowser (<xul:browser>)
+ *        The browser to query for toolbar visibility states
+ * @returns Promise
+ *        A promise that resolves when the toolbar state is retrieved
+ *        within the content context, which value is an object that holds
+ *        the visibility state of the toolbars
+ */
+function getToolbarsFromBrowserContent(aBrowser) {
+  return ContentTask.spawn(aBrowser, {}, function*() {
+    return {
+      toolbar: content.toolbar.visible,
+      menubar: content.menubar.visible,
+      personalbar: content.personalbar.visible,
+      statusbar: content.statusbar.visible,
+      locationbar: content.locationbar.visible,
+    };
+  });
+}
+
+/**
+ * This function retrieves the visibility state of the toolbars of a
+ * window within the chrome context.
+ *
+ * @param win
+ *        the chrome privileged window
+ * @returns object
+ *        an object that holds the visibility state of the toolbars
+ */
+function getToolbarsFromWindowChrome(win) {
+  return {
+    toolbar: win.toolbar.visible,
+    menubar: win.menubar.visible,
+    personalbar: win.personalbar.visible,
+    statusbar: win.statusbar.visible,
+    locationbar: win.locationbar.visible,
+  }
+}
+
+/**
+ * Tests toolbar visibility when opening a window with default parameters.
+ *
+ * @param toolbars
+ *        the visibility state of the toolbar elements
+ */
+function testDefaultToolbars(toolbars) {
+  ok(toolbars.locationbar,
+     "locationbar should be visible on default window.open()");
+  ok(toolbars.menubar,
+     "menubar be visible on default window.open()");
+  ok(toolbars.personalbar,
+     "personalbar should be visible on default window.open()");
+  ok(toolbars.statusbar,
+     "statusbar should be visible on default window.open()");
+  ok(toolbars.toolbar,
+     "toolbar should be visible on default window.open()");
+}
+
+/**
+ * Tests toolbar visibility when opening a window with non default parameters
+ * on the content context.
+ *
+ * Ensure that locationbar can't be hidden in the content context, see bug#337344.
+ *
+ * @param toolbars
+ *        the visibility state of the toolbar elements
+ */
+function testNonDefaultContentToolbars(toolbars) {
+  // Locationbar should always be visible on content context
+  ok(toolbars.locationbar,
+     "locationbar should be visible even with location=no");
+  ok(!toolbars.menubar,
+     "menubar shouldn't be visible when menubar=no");
+  ok(!toolbars.personalbar,
+     "personalbar shouldn't be visible when personalbar=no");
+  // statusbar will report visible=true even when it's hidden because of bug#55820
+  todo(!toolbars.statusbar,
+       "statusbar shouldn't be visible when status=no");
+  ok(!toolbars.toolbar,
+     "toolbar shouldn't be visible when toolbar=no");
+}
+
+/**
+ * Tests toolbar visibility when opening a window with non default parameters
+ * on the chrome context.
+ *
+ * @param toolbars
+ *        the visibility state of the toolbar elements
+ */
+function testNonDefaultChromeToolbars(toolbars) {
+  // None of the toolbars should be visible if hidden with chrome privileges
+  ok(!toolbars.locationbar,
+     "locationbar should be visible on default window.open()");
+  ok(!toolbars.menubar,
+     "menubar be visible on default window.open()");
+  ok(!toolbars.personalbar,
+     "personalbar should be visible on default window.open()");
+  ok(!toolbars.statusbar,
+     "statusbar should be visible on default window.open()");
+  ok(!toolbars.toolbar,
+     "toolbar should be visible on default window.open()");
+}
+
+/**
+ * Ensure that toolbars of a window opened in the content context have the
+ * correct visibility.
+ *
+ * A window opened with default parameters should have all toolbars visible.
+ *
+ * A window opened with "location=no, personalbar=no, toolbar=no, scrollbars=no,
+ * menubar=no, status=no", should only have location visible.
+ */
+add_task(function*() {
+  yield BrowserTestUtils.withNewTab({
+    gBrowser,
+    url: CONTENT_PAGE,
+  }, function*(browser) {
+    // First, call the default window.open() which will open a new tab
+    let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
+    yield BrowserTestUtils.synthesizeMouseAtCenter("#winOpenDefault", {}, browser);
+    let tab = yield newTabPromise;
+
+    // Check that all toolbars are visible
+    let toolbars = yield getToolbarsFromBrowserContent(gBrowser.selectedBrowser);
+    testDefaultToolbars(toolbars);
+
+    // Cleanup
+    yield BrowserTestUtils.removeTab(tab);
+
+    // Now let's open a window with toolbars hidden
+    let winPromise = BrowserTestUtils.waitForNewWindow();
+    yield BrowserTestUtils.synthesizeMouseAtCenter("#winOpenNonDefault", {}, browser);
+    let popupWindow = yield winPromise;
+
+    let popupBrowser = popupWindow.gBrowser.selectedBrowser;
+    yield BrowserTestUtils.browserLoaded(popupBrowser);
+
+    // Test toolbars visibility
+    let popupToolbars = yield getToolbarsFromBrowserContent(popupBrowser);
+    testNonDefaultContentToolbars(popupToolbars);
+
+    // Cleanup
+    yield BrowserTestUtils.closeWindow(popupWindow);
+  });
+});
+
+/**
+ * Ensure that toolbars of a window opened in the chrome context have the
+ * correct visibility.
+ *
+ * A window opened with default parameters should have all toolbars visible.
+ *
+ * A window opened with "location=no, personalbar=no, toolbar=no, scrollbars=no,
+ * menubar=no, status=no", should not have any toolbar visible.
+ */
+add_task(function* () {
+  // First open a default window from this chrome context
+  let defaultWindowPromise = BrowserTestUtils.waitForNewWindow();
+  window.open("about:robots", "_blank");
+  let defaultWindow = yield defaultWindowPromise;
+
+  // Check that all toolbars are visible
+  let toolbars = getToolbarsFromWindowChrome(defaultWindow);
+  testDefaultToolbars(toolbars);
+
+  // Now lets open a window with toolbars hidden from this chrome context
+  let features = "location=no, personalbar=no, toolbar=no, scrollbars=no, menubar=no, status=no";
+  let popupWindowPromise = BrowserTestUtils.waitForNewWindow();
+  window.open("about:robots", "_blank", features);
+  let popupWindow = yield popupWindowPromise;
+
+  // Test none of the tooolbars are visible
+  let hiddenToolbars = getToolbarsFromWindowChrome(popupWindow);
+  testNonDefaultChromeToolbars(hiddenToolbars);
+
+  // Cleanup
+  yield BrowserTestUtils.closeWindow(defaultWindow);
+  yield BrowserTestUtils.closeWindow(popupWindow);
+});
--- a/dom/tests/browser/test_new_window_from_content_child.html
+++ b/dom/tests/browser/test_new_window_from_content_child.html
@@ -1,19 +1,19 @@
 <!DOCTYPE html>
 <head>
   <meta charset="utf-8">
   <title>Test popup window opening behaviour</title>
 </head>
 <body>
   <p><a id="winOpenDefault" href="#" onclick="return openWindow();">Open a new window via window.open with default features.</a></p>
-  <p><a id="winOpenNonDefault" href="#" onclick="return openWindow('resizable=no, toolbar=no, scrollbars=no, menubar=no, status=no, directories=no, height=100, width=500');">Open a new window via window.open with non-default features.</a></p>
+  <p><a id="winOpenNonDefault" href="#" onclick="return openWindow('resizable=no, location=no, personalbar=no, toolbar=no, scrollbars=no, menubar=no, status=no, directories=no, height=100, width=500');">Open a new window via window.open with non-default features.</a></p>
   <p><a id="winOpenDialog" href="#" onclick="return openWindow('dialog=yes');">Open a new window via window.open with dialog=1.</a></p>
   <p><a id="targetBlank" href="about:robots" target="_blank">Open a new window via target="_blank".</a></p>
 </body>
 </html>
 
 <script>
 function openWindow(aFeatures="") {
   window.open("about:robots", "_blank", aFeatures);
   return false;
 }
-</script>
\ No newline at end of file
+</script>
--- a/dom/workers/ServiceWorkerManager.cpp
+++ b/dom/workers/ServiceWorkerManager.cpp
@@ -1521,17 +1521,17 @@ ServiceWorkerManager::Register(nsIDOMWin
 
   nsCString cleanedScope;
   rv = aScopeURI->GetSpecIgnoringRef(cleanedScope);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return NS_ERROR_FAILURE;
   }
 
   nsAutoCString spec;
-  rv = aScriptURI->GetSpec(spec);
+  rv = aScriptURI->GetSpecIgnoringRef(spec);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   nsCOMPtr<nsIGlobalObject> sgo = do_QueryInterface(window);
   ErrorResult result;
   RefPtr<Promise> promise = Promise::Create(sgo, result);
   if (result.Failed()) {
--- a/gfx/2d/Logging.h
+++ b/gfx/2d/Logging.h
@@ -129,17 +129,22 @@ private:
 /// we will still send a printf.
 
 // The range is due to the values set in Histograms.json
 enum class LogReason : int {
   MustBeMoreThanThis = -1,
   // Start.  Do not insert, always add at end.  If you remove items,
   // make sure the other items retain their values.
   D3D11InvalidCallDeviceRemoved = 0,
-  D3D11InvalidCall = 1,
+  D3D11InvalidCall,
+  D3DLockTimeout,
+  D3D10FinalizeFrame,
+  D3D11FinalizeFrame,
+  D3D10SyncLock,
+  D3D11SyncLock,
   // End
   MustBeLessThanThis = 101,
 };
 
 struct BasicLogger
 {
   // For efficiency, this method exists and copies the logic of the
   // OutputMessage below.  If making any changes here, also make it
@@ -560,17 +565,17 @@ typedef Log<LOG_CRITICAL, CriticalLogger
 #define gfxWarningOnce static gfxWarning GFX_LOGGING_GLUE(sOnceAtLine,__LINE__) = gfxWarning
 
 // In the debug build, this is equivalent to the default gfxCriticalError.
 // In the non-debug build, on nightly and dev edition, it will MOZ_CRASH.
 // On beta and release versions, it will telemetry count, but proceed.
 //
 // You should create a (new) enum in the LogReason and use it for the reason
 // parameter to ensure uniqueness.
-#define gfxDevCrash(reason) gfxCriticalError(int(LogOptions::AutoPrefix) | int(LogOptions::AssertOnCall) | int(LogOptions::CrashAction), (reason))
+#define gfxDevCrash(reason) gfxCriticalError(int(gfx::LogOptions::AutoPrefix) | int(gfx::LogOptions::AssertOnCall) | int(gfx::LogOptions::CrashAction), (reason))
 
 // See nsDebug.h and the NS_WARN_IF macro
 
 #ifdef __cplusplus
  // For now, have MOZ2D_ERROR_IF available in debug and non-debug builds
 inline bool MOZ2D_error_if_impl(bool aCondition, const char* aExpr,
                                 const char* aFile, int32_t aLine)
 {
--- a/gfx/layers/ImageContainer.cpp
+++ b/gfx/layers/ImageContainer.cpp
@@ -645,22 +645,21 @@ CairoImage::GetTextureClient(Compositabl
                                                            surface->GetSize(),
                                                            BackendSelector::Content,
                                                            TextureFlags::DEFAULT);
   }
   if (!textureClient) {
     return nullptr;
   }
 
-  if (!textureClient->Lock(OpenMode::OPEN_WRITE_ONLY)) {
+  TextureClientAutoLock autoLock(textureClient, OpenMode::OPEN_WRITE_ONLY);
+  if (!autoLock.Succeeded()) {
     return nullptr;
   }
 
-  TextureClientAutoUnlock autoUnlock(textureClient);
-
   textureClient->UpdateFromSurface(surface);
 
   textureClient->SyncWithObject(forwarder->GetSyncObject());
 
   mTextureClients.Put(forwarder->GetSerial(), textureClient);
   return textureClient;
 }
 
--- a/gfx/layers/client/CanvasClient.cpp
+++ b/gfx/layers/client/CanvasClient.cpp
@@ -93,32 +93,30 @@ CanvasClient2D::Update(gfx::IntSize aSiz
       NS_WARNING("Failed to allocate the TextureClient");
       return;
     }
     MOZ_ASSERT(mBuffer->CanExposeDrawTarget());
 
     bufferCreated = true;
   }
 
-  if (!mBuffer->Lock(OpenMode::OPEN_WRITE_ONLY)) {
-    mBuffer = nullptr;
-    return;
-  }
-
   bool updated = false;
   {
-    // Restrict drawTarget to a scope so that terminates before Unlock.
-    RefPtr<DrawTarget> target =
-      mBuffer->BorrowDrawTarget();
+    TextureClientAutoLock autoLock(mBuffer, OpenMode::OPEN_WRITE_ONLY);
+    if (!autoLock.Succeeded()) {
+      mBuffer = nullptr;
+      return;
+    }
+
+    RefPtr<DrawTarget> target = mBuffer->BorrowDrawTarget();
     if (target) {
       aLayer->UpdateTarget(target);
       updated = true;
     }
   }
-  mBuffer->Unlock();
 
   if (bufferCreated && !AddTextureClient(mBuffer)) {
     mBuffer = nullptr;
     return;
   }
 
   if (updated) {
     nsAutoTArray<CompositableForwarder::TimedTextureClient,1> textures;
@@ -281,17 +279,19 @@ TexClientFromReadback(SharedSurface* src
       MOZ_CRASH("Bad `read{Format,Type}`.");
     }
 
     MOZ_ASSERT(texClient);
     if (!texClient)
         return nullptr;
 
     // With a texClient, we can lock for writing.
-    MOZ_ALWAYS_TRUE( texClient->Lock(OpenMode::OPEN_WRITE) );
+    TextureClientAutoLock autoLock(texClient, OpenMode::OPEN_WRITE);
+    DebugOnly<bool> succeeded = autoLock.Succeeded();
+    MOZ_ASSERT(succeeded, "texture should have locked");
 
     uint8_t* lockedBytes = texClient->GetLockedData();
 
     // ReadPixels from the current FB into lockedBits.
     auto width = src->mSize.width;
     auto height = src->mSize.height;
 
     {
@@ -313,18 +313,16 @@ TexClientFromReadback(SharedSurface* src
       uint8_t* itr = lockedBytes;
       for (size_t i = 0; i < pixels; i++) {
         SwapRB_R8G8B8A8(itr);
         itr += 4;
       }
 
       texClient->RemoveFlags(TextureFlags::RB_SWAPPED);
     }
-
-    texClient->Unlock();
   }
 
   return texClient.forget();
 }
 
 ////////////////////////////////////////
 
 static already_AddRefed<SharedSurfaceTextureClient>
--- a/gfx/layers/client/ContentClient.cpp
+++ b/gfx/layers/client/ContentClient.cpp
@@ -9,16 +9,17 @@
 #include "gfxPlatform.h"                // for gfxPlatform
 #include "gfxEnv.h"                     // for gfxEnv
 #include "gfxPrefs.h"                   // for gfxPrefs
 #include "gfxPoint.h"                   // for IntSize, gfxPoint
 #include "gfxTeeSurface.h"              // for gfxTeeSurface
 #include "gfxUtils.h"                   // for gfxUtils
 #include "ipc/ShadowLayers.h"           // for ShadowLayerForwarder
 #include "mozilla/ArrayUtils.h"         // for ArrayLength
+#include "mozilla/Maybe.h"
 #include "mozilla/gfx/2D.h"             // for DrawTarget, Factory
 #include "mozilla/gfx/BasePoint.h"      // for BasePoint
 #include "mozilla/gfx/BaseSize.h"       // for BaseSize
 #include "mozilla/gfx/Rect.h"           // for Rect
 #include "mozilla/gfx/Types.h"
 #include "mozilla/layers/CompositorChild.h" // for CompositorChild
 #include "mozilla/layers/LayerManagerComposite.h"
 #include "mozilla/layers/LayersMessages.h"  // for ThebesBufferData
@@ -565,43 +566,40 @@ ContentClientDoubleBuffered::FinalizeFra
   // nothing to sync at all, there is nothing to do and we can go home early.
   updateRegion.Sub(updateRegion, aRegionToDraw);
   if (updateRegion.IsEmpty()) {
     return;
   }
 
   // We need to ensure that we lock these two buffers in the same
   // order as the compositor to prevent deadlocks.
-  if (!mFrontClient->Lock(OpenMode::OPEN_READ_ONLY)) {
-    return;
-  }
-  if (mFrontClientOnWhite &&
-      !mFrontClientOnWhite->Lock(OpenMode::OPEN_READ_ONLY)) {
-    mFrontClient->Unlock();
+  TextureClientAutoLock frontLock(mFrontClient, OpenMode::OPEN_READ_ONLY);
+  if (!frontLock.Succeeded()) {
     return;
   }
-  {
-    // Restrict the DrawTargets and frontBuffer to a scope to make
-    // sure there is no more external references to the DrawTargets
-    // when we Unlock the TextureClients.
-    RefPtr<SourceSurface> surf = mFrontClient->BorrowDrawTarget()->Snapshot();
-    RefPtr<SourceSurface> surfOnWhite = mFrontClientOnWhite
-      ? mFrontClientOnWhite->BorrowDrawTarget()->Snapshot()
-      : nullptr;
-    SourceRotatedBuffer frontBuffer(surf,
-                                    surfOnWhite,
-                                    mFrontBufferRect,
-                                    mFrontBufferRotation);
-    UpdateDestinationFrom(frontBuffer, updateRegion);
+  Maybe<TextureClientAutoLock> frontOnWhiteLock;
+  if (mFrontClientOnWhite) {
+    frontOnWhiteLock.emplace(mFrontClientOnWhite, OpenMode::OPEN_READ_ONLY);
+    if (!frontOnWhiteLock->Succeeded()) {
+      return;
+    }
   }
 
-  mFrontClient->Unlock();
-  if (mFrontClientOnWhite) {
-    mFrontClientOnWhite->Unlock();
-  }
+  // Restrict the DrawTargets and frontBuffer to a scope to make
+  // sure there is no more external references to the DrawTargets
+  // when we Unlock the TextureClients.
+  RefPtr<SourceSurface> surf = mFrontClient->BorrowDrawTarget()->Snapshot();
+  RefPtr<SourceSurface> surfOnWhite = mFrontClientOnWhite
+    ? mFrontClientOnWhite->BorrowDrawTarget()->Snapshot()
+    : nullptr;
+  SourceRotatedBuffer frontBuffer(surf,
+                                  surfOnWhite,
+                                  mFrontBufferRect,
+                                  mFrontBufferRotation);
+  UpdateDestinationFrom(frontBuffer, updateRegion);
 }
 
 void
 ContentClientDoubleBuffered::EnsureBackBufferIfFrontBuffer()
 {
   if (!mTextureClient && mFrontClient) {
     CreateBackBuffer(mFrontBufferRect);
 
--- a/gfx/layers/client/ImageClient.cpp
+++ b/gfx/layers/client/ImageClient.cpp
@@ -180,27 +180,30 @@ ImageClientSingle::UpdateImage(ImageCont
         const PlanarYCbCrData* data = ycbcr->GetData();
         if (!data) {
           return false;
         }
         texture = TextureClient::CreateForYCbCr(GetForwarder(),
           data->mYSize, data->mCbCrSize, data->mStereoMode,
           TextureFlags::DEFAULT | mTextureFlags
         );
-        if (!texture || !texture->Lock(OpenMode::OPEN_WRITE_ONLY)) {
+        if (!texture) {
           return false;
         }
+
+        TextureClientAutoLock autoLock(texture, OpenMode::OPEN_WRITE_ONLY);
+        if (!autoLock.Succeeded()) {
+          return false;
+        }
+
         bool status = texture->AsTextureClientYCbCr()->UpdateYCbCr(*data);
         MOZ_ASSERT(status);
-
-        texture->Unlock();
         if (!status) {
           return false;
         }
-
       } else if (image->GetFormat() == ImageFormat::SURFACE_TEXTURE ||
                  image->GetFormat() == ImageFormat::EGLIMAGE) {
         gfx::IntSize size = image->GetSize();
 
         if (image->GetFormat() == ImageFormat::EGLIMAGE) {
           EGLImageImage* typedImage = static_cast<EGLImageImage*>(image);
           texture = new EGLImageTextureClient(GetForwarder(),
                                               mTextureFlags,
--- a/gfx/layers/client/TextureClient.h
+++ b/gfx/layers/client/TextureClient.h
@@ -7,16 +7,17 @@
 #define MOZILLA_GFX_TEXTURECLIENT_H
 
 #include <stddef.h>                     // for size_t
 #include <stdint.h>                     // for uint32_t, uint8_t, uint64_t
 #include "GLTextureImage.h"             // for TextureImage
 #include "ImageTypes.h"                 // for StereoMode
 #include "mozilla/Assertions.h"         // for MOZ_ASSERT, etc
 #include "mozilla/Attributes.h"         // for override
+#include "mozilla/DebugOnly.h"
 #include "mozilla/RefPtr.h"             // for RefPtr, RefCounted
 #include "mozilla/gfx/2D.h"             // for DrawTarget
 #include "mozilla/gfx/Point.h"          // for IntSize
 #include "mozilla/gfx/Types.h"          // for SurfaceFormat
 #include "mozilla/layers/FenceUtils.h"  // for FenceHandle
 #include "mozilla/ipc/Shmem.h"          // for Shmem
 #include "mozilla/layers/AtomicRefCountedWithFinalize.h"
 #include "mozilla/layers/CompositorTypes.h"  // for TextureFlags, etc
@@ -715,27 +716,49 @@ public:
 
   virtual bool HasInternalBuffer() const override { return true; }
 
 protected:
   uint8_t* mBuffer;
   size_t mBufSize;
 };
 
-struct TextureClientAutoUnlock
+// Automatically lock and unlock a texture. Since texture locking is fallible,
+// Succeeded() must be checked on the guard object before proceeding.
+class MOZ_RAII TextureClientAutoLock
 {
-  TextureClient* mTexture;
+  MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER;
+
+public:
+  TextureClientAutoLock(TextureClient* aTexture, OpenMode aMode
+                        MOZ_GUARD_OBJECT_NOTIFIER_PARAM)
+   : mTexture(aTexture),
+     mSucceeded(false)
+  {
+    MOZ_GUARD_OBJECT_NOTIFIER_INIT;
 
-  explicit TextureClientAutoUnlock(TextureClient* aTexture)
-  : mTexture(aTexture) {}
+    mSucceeded = mTexture->Lock(aMode);
+    mChecked = false;
+  }
+  ~TextureClientAutoLock() {
+    MOZ_ASSERT(mChecked);
+    if (mSucceeded) {
+      mTexture->Unlock();
+    }
+  }
 
-  ~TextureClientAutoUnlock()
-  {
-    mTexture->Unlock();
+  bool Succeeded() {
+    mChecked = true;
+    return mSucceeded;
   }
+
+private:
+  TextureClient* mTexture;
+  DebugOnly<bool> mChecked;
+  bool mSucceeded;
 };
 
 class KeepAlive
 {
 public:
   virtual ~KeepAlive() {}
 };
 
--- a/gfx/layers/client/TiledContentClient.cpp
+++ b/gfx/layers/client/TiledContentClient.cpp
@@ -596,30 +596,29 @@ TileClient::Flip()
   mInvalidBack = invalidFront;
 }
 
 static bool
 CopyFrontToBack(TextureClient* aFront,
                 TextureClient* aBack,
                 const gfx::IntRect& aRectToCopy)
 {
-  if (!aFront->Lock(OpenMode::OPEN_READ)) {
+  TextureClientAutoLock frontLock(aFront, OpenMode::OPEN_READ);
+  if (!frontLock.Succeeded()) {
     gfxCriticalError() << "[Tiling:Client] Failed to lock the tile's front buffer";
     return false;
   }
 
   if (!aBack->Lock(OpenMode::OPEN_READ_WRITE)) {
     gfxCriticalError() << "[Tiling:Client] Failed to lock the tile's back buffer";
     return false;
   }
 
   gfx::IntPoint rectToCopyTopLeft = aRectToCopy.TopLeft();
   aFront->CopyToTextureClient(aBack, &aRectToCopy, &rectToCopyTopLeft);
-
-  aFront->Unlock();
   return true;
 }
 
 void
 TileClient::ValidateBackBufferFromFront(const nsIntRegion& aDirtyRegion,
                                         nsIntRegion& aAddPaintedRegion)
 {
   if (mBackBuffer && mFrontBuffer) {
--- a/gfx/layers/d3d11/TextureD3D11.cpp
+++ b/gfx/layers/d3d11/TextureD3D11.cpp
@@ -127,17 +127,17 @@ static bool LockD3DTexture(T* aTexture)
 {
   MOZ_ASSERT(aTexture);
   RefPtr<IDXGIKeyedMutex> mutex;
   aTexture->QueryInterface((IDXGIKeyedMutex**)getter_AddRefs(mutex));
   // Textures created by the DXVA decoders don't have a mutex for synchronization
   if (mutex) {
     HRESULT hr = mutex->AcquireSync(0, 10000);
     if (hr == WAIT_TIMEOUT) {
-      MOZ_CRASH();
+      gfxDevCrash(LogReason::D3DLockTimeout) << "D3D lock mutex timeout";
     }
 
     if (FAILED(hr)) {
       NS_WARNING("Failed to lock the texture");
       return false;
     }
   }
   return true;
@@ -1240,65 +1240,69 @@ SyncObjectD3D11::FinalizeFrame()
     
     if (FAILED(hr) || !mD3D10Texture) {
       gfxCriticalError() << "Failed to D3D10 OpenSharedResource for frame finalization: " << hexa(hr);
 
       if (gfxWindowsPlatform::GetPlatform()->DidRenderingDeviceReset()) {
         return;
       }
 
-      MOZ_CRASH();
+      gfxDevCrash(LogReason::D3D10FinalizeFrame) << "Without device reset: " << hexa(hr);
     }
 
     // test QI
     RefPtr<IDXGIKeyedMutex> mutex;
     hr = mD3D10Texture->QueryInterface((IDXGIKeyedMutex**)getter_AddRefs(mutex));
 
     if (FAILED(hr) || !mutex) {
+      // Leave both the critical error and MOZ_CRASH for now; the critical error lets
+      // us "save" the hr value.  We will probably eventuall replace this with gfxDevCrash.
       gfxCriticalError() << "Failed to get KeyedMutex: " << hexa(hr);
-      MOZ_CRASH();
+      MOZ_CRASH("GFX: Cannot get D3D10 KeyedMutex");
     }
   }
 
   if (!mD3D11Texture && mD3D11SyncedTextures.size()) {
     ID3D11Device* device = gfxWindowsPlatform::GetPlatform()->GetD3D11ContentDevice();
 
     hr = device->OpenSharedResource(mHandle, __uuidof(ID3D11Texture2D), (void**)(ID3D11Texture2D**)getter_AddRefs(mD3D11Texture));
     
     if (FAILED(hr) || !mD3D11Texture) {
       gfxCriticalError() << "Failed to D3D11 OpenSharedResource for frame finalization: " << hexa(hr);
 
       if (gfxWindowsPlatform::GetPlatform()->DidRenderingDeviceReset()) {
         return;
       }
 
-      MOZ_CRASH();
+      gfxDevCrash(LogReason::D3D11FinalizeFrame) << "Without device reset: " << hexa(hr);
     }
 
     // test QI
     RefPtr<IDXGIKeyedMutex> mutex;
     hr = mD3D11Texture->QueryInterface((IDXGIKeyedMutex**)getter_AddRefs(mutex));
 
     if (FAILED(hr) || !mutex) {
+      // Leave both the critical error and MOZ_CRASH for now; the critical error lets
+      // us "save" the hr value.  We will probably eventuall replace this with gfxDevCrash.
       gfxCriticalError() << "Failed to get KeyedMutex: " << hexa(hr);
-      MOZ_CRASH();
+      MOZ_CRASH("GFX: Cannot get D3D11 KeyedMutex");
     }
   }
 
   if (mD3D10SyncedTextures.size()) {
     RefPtr<IDXGIKeyedMutex> mutex;
     hr = mD3D10Texture->QueryInterface((IDXGIKeyedMutex**)getter_AddRefs(mutex));
     hr = mutex->AcquireSync(0, 20000);
 
     if (hr == WAIT_TIMEOUT) {
       if (gfxWindowsPlatform::GetPlatform()->DidRenderingDeviceReset()) {
         gfxWarning() << "AcquireSync timed out because of device reset.";
         return;
       }
-      MOZ_CRASH();
+      gfxDevCrash(LogReason::D3D10SyncLock) << "Timeout on the D3D10 sync lock";
     }
 
     D3D10_BOX box;
     box.front = box.top = box.left = 0;
     box.back = box.bottom = box.right = 1;
 
     ID3D10Device* device = gfxWindowsPlatform::GetPlatform()->GetD3D10Device();
 
@@ -1316,30 +1320,30 @@ SyncObjectD3D11::FinalizeFrame()
     hr = mD3D11Texture->QueryInterface((IDXGIKeyedMutex**)getter_AddRefs(mutex));
     hr = mutex->AcquireSync(0, 20000);
 
     if (hr == WAIT_TIMEOUT) {
       if (gfxWindowsPlatform::GetPlatform()->DidRenderingDeviceReset()) {
         gfxWarning() << "AcquireSync timed out because of device reset.";
         return;
       }
-      MOZ_CRASH();
+      gfxDevCrash(LogReason::D3D11SyncLock) << "Timeout on the D3D11 sync lock";
     }
 
     D3D11_BOX box;
     box.front = box.top = box.left = 0;
     box.back = box.bottom = box.right = 1;
 
     ID3D11Device* dev = gfxWindowsPlatform::GetPlatform()->GetD3D11ContentDevice();
 
     if (!dev) {
       if (gfxWindowsPlatform::GetPlatform()->DidRenderingDeviceReset()) {
         return;
       }
-      MOZ_CRASH();
+      MOZ_CRASH("GFX: Invalid D3D11 content device");
     }
 
     RefPtr<ID3D11DeviceContext> ctx;
     dev->GetImmediateContext(getter_AddRefs(ctx));
 
     for (auto iter = mD3D11SyncedTextures.begin(); iter != mD3D11SyncedTextures.end(); iter++) {
       ctx->CopySubresourceRegion(mD3D11Texture, 0, 0, 0, 0, *iter, 0, &box);
     }
--- a/gfx/layers/ipc/SharedPlanarYCbCrImage.cpp
+++ b/gfx/layers/ipc/SharedPlanarYCbCrImage.cpp
@@ -85,21 +85,23 @@ SharedPlanarYCbCrImage::SetData(const Pl
   // allocate it. This code path is slower than the one used when Allocate has
   // been called since it will trigger a full copy.
   PlanarYCbCrData data = aData;
   if (!mTextureClient && !Allocate(data)) {
     return false;
   }
 
   MOZ_ASSERT(mTextureClient->AsTextureClientYCbCr());
-  if (!mTextureClient->Lock(OpenMode::OPEN_WRITE_ONLY)) {
+
+  TextureClientAutoLock autoLock(mTextureClient, OpenMode::OPEN_WRITE_ONLY);
+  if (!autoLock.Succeeded()) {
     MOZ_ASSERT(false, "Failed to lock the texture.");
     return false;
   }
-  TextureClientAutoUnlock unlock(mTextureClient);
+
   if (!mTextureClient->AsTextureClientYCbCr()->UpdateYCbCr(aData)) {
     MOZ_ASSERT(false, "Failed to copy YCbCr data into the TextureClient");
     return false;
   }
   mTextureClient->MarkImmutable();
   return true;
 }
 
--- a/gfx/thebes/gfx2DGlue.h
+++ b/gfx/thebes/gfx2DGlue.h
@@ -46,21 +46,31 @@ inline gfxMatrix ThebesMatrix(const Matr
                    aMatrix._22, aMatrix._31, aMatrix._32);
 }
 
 inline Point ToPoint(const gfxPoint &aPoint)
 {
   return Point(Float(aPoint.x), Float(aPoint.y));
 }
 
+inline Size ToSize(const gfxSize &aSize)
+{
+  return Size(Float(aSize.width), Float(aSize.height));
+}
+
 inline gfxPoint ThebesPoint(const Point &aPoint)
 {
   return gfxPoint(aPoint.x, aPoint.y);
 }
 
+inline gfxSize ThebesSize(const Size &aSize)
+{
+  return gfxSize(aSize.width, aSize.height);
+}
+
 inline gfxRect ThebesRect(const Rect &aRect)
 {
   return gfxRect(aRect.x, aRect.y, aRect.width, aRect.height);
 }
 
 inline gfxRect ThebesRect(const RectDouble &aRect)
 {
   return gfxRect(aRect.x, aRect.y, aRect.width, aRect.height);
--- a/gfx/thebes/gfxFcPlatformFontList.cpp
+++ b/gfx/thebes/gfxFcPlatformFontList.cpp
@@ -842,41 +842,54 @@ gfxFontconfigFontFamily::FindStyleVariat
 {
     if (mHasStyles) {
         return;
     }
 
     // add font entries for each of the faces
     uint32_t numFonts = mFontPatterns.Length();
     NS_ASSERTION(numFonts, "font family containing no faces!!");
+    uint32_t numRegularFaces = 0;
     for (uint32_t i = 0; i < numFonts; i++) {
         FcPattern* face = mFontPatterns[i];
 
         // figure out the psname/fullname and choose which to use as the facename
         nsAutoString psname, fullname;
         GetFaceNames(face, mName, psname, fullname);
         const nsAutoString& faceName = !psname.IsEmpty() ? psname : fullname;
 
         gfxFontconfigFontEntry *fontEntry =
             new gfxFontconfigFontEntry(faceName, face);
         AddFontEntry(fontEntry);
 
+        if (fontEntry->IsUpright() &&
+            fontEntry->Weight() == NS_FONT_WEIGHT_NORMAL &&
+            fontEntry->Stretch() == NS_FONT_STRETCH_NORMAL) {
+            numRegularFaces++;
+        }
+
         if (LOG_FONTLIST_ENABLED()) {
             LOG_FONTLIST(("(fontlist) added (%s) to family (%s)"
                  " with style: %s weight: %d stretch: %d"
                  " psname: %s fullname: %s",
                  NS_ConvertUTF16toUTF8(fontEntry->Name()).get(),
                  NS_ConvertUTF16toUTF8(Name()).get(),
                  (fontEntry->IsItalic()) ?
                   "italic" : (fontEntry->IsOblique() ? "oblique" : "normal"),
                  fontEntry->Weight(), fontEntry->Stretch(),
                  NS_ConvertUTF16toUTF8(psname).get(),
                  NS_ConvertUTF16toUTF8(fullname).get()));
         }
     }
+
+    // somewhat arbitrary, but define a family with two or more regular
+    // faces as a family for which intra-family fallback should be used
+    if (numRegularFaces > 1) {
+        mCheckForFallbackFaces = true;
+    }
     mFaceNamesInitialized = true;
     mFontPatterns.Clear();
     SetHasStyles(true);
 }
 
 void
 gfxFontconfigFontFamily::AddFontPattern(FcPattern* aFontPattern)
 {
--- a/gfx/thebes/gfxFontEntry.h
+++ b/gfx/thebes/gfxFontEntry.h
@@ -658,17 +658,18 @@ public:
         mName(aName),
         mOtherFamilyNamesInitialized(false),
         mHasOtherFamilyNames(false),
         mFaceNamesInitialized(false),
         mHasStyles(false),
         mIsSimpleFamily(false),
         mIsBadUnderlineFamily(false),
         mFamilyCharacterMapInitialized(false),
-        mSkipDefaultFeatureSpaceCheck(false)
+        mSkipDefaultFeatureSpaceCheck(false),
+        mCheckForFallbackFaces(false)
         { }
 
     const nsString& Name() { return mName; }
 
     virtual void LocalizedName(nsAString& aLocalizedName);
     virtual bool HasOtherFamilyNames();
     
     nsTArray<RefPtr<gfxFontEntry> >& GetFontList() { return mAvailableFonts; }
@@ -762,16 +763,17 @@ public:
     void SetBadUnderlineFamily() {
         mIsBadUnderlineFamily = true;
         if (mHasStyles) {
             SetBadUnderlineFonts();
         }
     }
 
     bool IsBadUnderlineFamily() const { return mIsBadUnderlineFamily; }
+    bool CheckForFallbackFaces() const { return mCheckForFallbackFaces; }
 
     // sort available fonts to put preferred (standard) faces towards the end
     void SortAvailableFonts();
 
     // check whether the family fits into the simple 4-face model,
     // so we can use simplified style-matching;
     // if so set the mIsSimpleFamily flag (defaults to False before we've checked)
     void CheckForSimpleFamily();
@@ -817,16 +819,17 @@ protected:
     bool mOtherFamilyNamesInitialized : 1;
     bool mHasOtherFamilyNames : 1;
     bool mFaceNamesInitialized : 1;
     bool mHasStyles : 1;
     bool mIsSimpleFamily : 1;
     bool mIsBadUnderlineFamily : 1;
     bool mFamilyCharacterMapInitialized : 1;
     bool mSkipDefaultFeatureSpaceCheck : 1;
+    bool mCheckForFallbackFaces : 1;  // check other faces for character
 
     enum {
         // for "simple" families, the faces are stored in mAvailableFonts
         // with fixed positions:
         kRegularFaceIndex    = 0,
         kBoldFaceIndex       = 1,
         kItalicFaceIndex     = 2,
         kBoldItalicFaceIndex = 3,
--- a/gfx/thebes/gfxTextRun.cpp
+++ b/gfx/thebes/gfxTextRun.cpp
@@ -1643,16 +1643,22 @@ gfxFontGroup::AddFamilyToFontList(gfxFon
         if (!HasFont(fe)) {
             FamilyFace ff(aFamily, fe, needsBold);
             if (fe->mIsUserFontContainer) {
                 ff.CheckState(mSkipDrawing);
             }
             mFonts.AppendElement(ff);
         }
     }
+    // for a family marked as "check fallback faces", only mark the last
+    // entry so that fallbacks for a family are only checked once
+    if (aFamily->CheckForFallbackFaces() &&
+        !fontEntryList.IsEmpty() && !mFonts.IsEmpty()) {
+        mFonts.LastElement().SetCheckForFallbackFaces();
+    }
 }
 
 bool
 gfxFontGroup::HasFont(const gfxFontEntry *aFontEntry)
 {
     uint32_t count = mFonts.Length();
     for (uint32_t i = 0; i < count; ++i) {
         if (mFonts[i].FontEntry() == aFontEntry) {
@@ -2550,16 +2556,33 @@ gfxFontGroup::FindNonItalicFaceForChar(g
     if (!fe->HasCharacter(aCh)) {
         return nullptr;
     }
 
     RefPtr<gfxFont> font = fe->FindOrMakeFont(&mStyle, needsBold);
     return font.forget();
 }
 
+already_AddRefed<gfxFont>
+gfxFontGroup::FindFallbackFaceForChar(gfxFontFamily* aFamily, uint32_t aCh,
+                                      int32_t aRunScript)
+{
+    GlobalFontMatch data(aCh, aRunScript, &mStyle);
+    aFamily->SearchAllFontsForChar(&data);
+    gfxFontEntry* fe = data.mBestMatch;
+    if (!fe) {
+        return nullptr;
+    }
+
+    bool needsBold = mStyle.weight >= 600 && !fe->IsBold() &&
+                     mStyle.allowSyntheticWeight;
+    RefPtr<gfxFont> font = fe->FindOrMakeFont(&mStyle, needsBold);
+    return font.forget();
+}
+
 gfxFloat
 gfxFontGroup::GetUnderlineOffset()
 {
     if (mUnderlineOffset == UNDERLINE_OFFSET_NOT_SET) {
         // if the fontlist contains a bad underline font, make the underline
         // offset the min of the first valid font and bad font underline offsets
         uint32_t len = mFonts.Length();
         for (uint32_t i = 0; i < len; i++) {
@@ -2614,20 +2637,27 @@ gfxFontGroup::FindFontForChar(uint32_t a
     if (!isJoinControl && !wasJoinCauser && !isVarSelector) {
         RefPtr<gfxFont> firstFont = GetFontAt(0, aCh);
         if (firstFont) {
             if (firstFont->HasCharacter(aCh)) {
                 *aMatchType = gfxTextRange::kFontGroup;
                 return firstFont.forget();
             }
 
-            // If italic, test the regular face to see if it supports character.
-            // Only do this for platform fonts, not userfonts.
-            if (mStyle.style != NS_FONT_STYLE_NORMAL &&
-                !firstFont->GetFontEntry()->IsUserFont()) {
+            if (mFonts[0].CheckForFallbackFaces()) {
+                RefPtr<gfxFont> font =
+                    FindFallbackFaceForChar(mFonts[0].Family(), aCh, aRunScript);
+                if (font) {
+                    *aMatchType = gfxTextRange::kFontGroup;
+                    return font.forget();
+                }
+            } else if (mStyle.style != NS_FONT_STYLE_NORMAL &&
+                       !firstFont->GetFontEntry()->IsUserFont()) {
+                // If italic, test the regular face to see if it supports
+                // character. Only do this for platform fonts, not userfonts.
                 RefPtr<gfxFont> font =
                     FindNonItalicFaceForChar(mFonts[0].Family(), aCh);
                 if (font) {
                     *aMatchType = gfxTextRange::kFontGroup;
                     return font.forget();
                 }
             }
         }
@@ -2717,27 +2747,40 @@ gfxFontGroup::FindFontForChar(uint32_t a
             // build the font via GetFontAt
             font = GetFontAt(i, aCh);
             if (font) {
                 *aMatchType = gfxTextRange::kFontGroup;
                 return font.forget();
             }
         }
 
-        // If italic, test the regular face to see if it supports the character.
-        // Only do this for platform fonts, not userfonts.
-        fe = ff.FontEntry();
-        if (mStyle.style != NS_FONT_STYLE_NORMAL &&
-            !fe->mIsUserFontContainer &&
-            !fe->IsUserFont()) {
-            font = FindNonItalicFaceForChar(ff.Family(), aCh);
+        // check other family faces if needed
+        if (ff.CheckForFallbackFaces()) {
+            NS_ASSERTION(i == 0 ? true :
+                         !mFonts[i-1].CheckForFallbackFaces() ||
+                         !mFonts[i-1].Family()->Name().Equals(ff.Family()->Name()),
+                         "should only do fallback once per font family");
+            font = FindFallbackFaceForChar(ff.Family(), aCh, aRunScript);
             if (font) {
                 *aMatchType = gfxTextRange::kFontGroup;
                 return font.forget();
             }
+        } else {
+            // If italic, test the regular face to see if it supports the
+            // character. Only do this for platform fonts, not userfonts.
+            fe = ff.FontEntry();
+            if (mStyle.style != NS_FONT_STYLE_NORMAL &&
+                !fe->mIsUserFontContainer &&
+                !fe->IsUserFont()) {
+                font = FindNonItalicFaceForChar(ff.Family(), aCh);
+                if (font) {
+                    *aMatchType = gfxTextRange::kFontGroup;
+                    return font.forget();
+                }
+            }
         }
     }
 
     if (fontListLength == 0) {
         RefPtr<gfxFont> defaultFont = GetDefaultFont();
         if (defaultFont->HasCharacter(aCh)) {
             *aMatchType = gfxTextRange::kFontGroup;
             return defaultFont.forget();
--- a/gfx/thebes/gfxTextRun.h
+++ b/gfx/thebes/gfxTextRun.h
@@ -877,50 +877,52 @@ protected:
     void ComputeRanges(nsTArray<gfxTextRange>& mRanges,
                        const T *aString, uint32_t aLength,
                        int32_t aRunScript, uint16_t aOrientation);
 
     class FamilyFace {
     public:
         FamilyFace() : mFamily(nullptr), mFontEntry(nullptr),
                        mNeedsBold(false), mFontCreated(false),
-                       mLoading(false), mInvalid(false)
+                       mLoading(false), mInvalid(false),
+                       mCheckForFallbackFaces(false)
         { }
 
         FamilyFace(gfxFontFamily* aFamily, gfxFont* aFont)
             : mFamily(aFamily), mNeedsBold(false), mFontCreated(true),
-              mLoading(false), mInvalid(false)
+              mLoading(false), mInvalid(false), mCheckForFallbackFaces(false)
         {
             NS_ASSERTION(aFont, "font pointer must not be null");
             NS_ASSERTION(!aFamily ||
                          aFamily->ContainsFace(aFont->GetFontEntry()),
                          "font is not a member of the given family");
             mFont = aFont;
             NS_ADDREF(aFont);
         }
 
         FamilyFace(gfxFontFamily* aFamily, gfxFontEntry* aFontEntry,
                    bool aNeedsBold)
             : mFamily(aFamily), mNeedsBold(aNeedsBold), mFontCreated(false),
-              mLoading(false), mInvalid(false)
+              mLoading(false), mInvalid(false), mCheckForFallbackFaces(false)
         {
             NS_ASSERTION(aFontEntry, "font entry pointer must not be null");
             NS_ASSERTION(!aFamily ||
                          aFamily->ContainsFace(aFontEntry),
                          "font is not a member of the given family");
             mFontEntry = aFontEntry;
             NS_ADDREF(aFontEntry);
         }
 
         FamilyFace(const FamilyFace& aOtherFamilyFace)
             : mFamily(aOtherFamilyFace.mFamily),
               mNeedsBold(aOtherFamilyFace.mNeedsBold),
               mFontCreated(aOtherFamilyFace.mFontCreated),
               mLoading(aOtherFamilyFace.mLoading),
-              mInvalid(aOtherFamilyFace.mInvalid)
+              mInvalid(aOtherFamilyFace.mInvalid),
+              mCheckForFallbackFaces(aOtherFamilyFace.mCheckForFallbackFaces)
         {
             if (mFontCreated) {
                 mFont = aOtherFamilyFace.mFont;
                 NS_ADDREF(mFont);
             } else {
                 mFontEntry = aOtherFamilyFace.mFontEntry;
                 NS_IF_ADDREF(mFontEntry);
             }
@@ -973,16 +975,18 @@ protected:
         bool IsUserFontContainer() const {
             return FontEntry()->mIsUserFontContainer;
         }
         bool IsLoading() const { return mLoading; }
         bool IsInvalid() const { return mInvalid; }
         void CheckState(bool& aSkipDrawing);
         void SetLoading(bool aIsLoading) { mLoading = aIsLoading; }
         void SetInvalid() { mInvalid = true; }
+        bool CheckForFallbackFaces() const { return mCheckForFallbackFaces; }
+        void SetCheckForFallbackFaces() { mCheckForFallbackFaces = true; }
 
         void SetFont(gfxFont* aFont)
         {
             NS_ASSERTION(aFont, "font pointer must not be null");
             NS_ADDREF(aFont);
             if (mFontCreated) {
                 NS_RELEASE(mFont);
             } else {
@@ -1001,16 +1005,17 @@ protected:
         union {
             gfxFont*            mFont;
             gfxFontEntry*       mFontEntry;
         };
         bool                    mNeedsBold   : 1;
         bool                    mFontCreated : 1;
         bool                    mLoading     : 1;
         bool                    mInvalid     : 1;
+        bool                    mCheckForFallbackFaces : 1;
     };
 
     // List of font families, either named or generic.
     // Generic names map to system pref fonts based on language.
     mozilla::FontFamilyList mFamilyList;
 
     // Fontlist containing a font entry for each family found. gfxFont objects
     // are created as needed and userfont loads are initiated when needed.
@@ -1097,17 +1102,23 @@ protected:
 
     // Helper for font-matching:
     // When matching the italic case, allow use of the regular face
     // if it supports a character but the italic one doesn't.
     // Return null if regular face doesn't support aCh
     already_AddRefed<gfxFont>
     FindNonItalicFaceForChar(gfxFontFamily* aFamily, uint32_t aCh);
 
-    // helper methods for looking up fonts
+    // search all faces in a family for a fallback in cases where it's unclear
+    // whether the family might have a font for a given character
+    already_AddRefed<gfxFont>
+    FindFallbackFaceForChar(gfxFontFamily* aFamily, uint32_t aCh,
+                            int32_t aRunScript);
+
+   // helper methods for looking up fonts
 
     // lookup and add a font with a given name (i.e. *not* a generic!)
     void AddPlatformFont(const nsAString& aName,
                          nsTArray<gfxFontFamily*>& aFamilyList);
 
     // do style selection and add entries to list
     void AddFamilyToFontList(gfxFontFamily* aFamily);
 
deleted file mode 100644
--- a/ipc/chromium/Makefile.in
+++ /dev/null
@@ -1,40 +0,0 @@
-# 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/.
-
-OS_CXXFLAGS := $(filter-out -fshort-wchar,$(OS_CXXFLAGS))
-
-ACDEFINES =
-
-ifndef MOZ_NATIVE_LIBEVENT # {
-vpath %.c \
-  $(srcdir)/src/third_party/libevent \
-  $(NULL)
-endif # }
-
-vpath %.cc \
-  $(srcdir)/src/base \
-  $(srcdir)/src/chrome/common \
-  $(NULL)
-
-vpath %.mm \
-  $(srcdir)/src/base \
-  $(srcdir)/src/chrome/common \
-  $(NULL)
-
-OS_CXXFLAGS += $(TK_CFLAGS)
-
-include $(topsrcdir)/config/rules.mk
-
-ifdef MOZ_NATIVE_LIBEVENT # {
-
-export-preqs = \
-  $(call mkdir_deps,$(CURDIR)/third_party/libevent) \
-  $(NULL)
-
-export:: $(DIST)/third_party/libevent/event.h
-
-$(DIST)/third_party/libevent/event.h:: $(export-preqs)
-	echo '#include <event.h>' > $(CURDIR)/third_party/libevent/event.h
-
-endif # }
--- a/ipc/chromium/moz.build
+++ b/ipc/chromium/moz.build
@@ -162,11 +162,13 @@ if os_bsd or os_linux:
         ]
 
 ost = CONFIG['OS_TEST']
 if '86' not in ost and 'arm' not in ost and 'mips' not in ost:
     SOURCES += [
         'src/base/atomicops_internals_mutex.cc',
     ]
 
+CXXFLAGS += CONFIG['TK_CFLAGS']
+
 include('/ipc/chromium/chromium-config.mozbuild')
 
 FINAL_LIBRARY = 'xul'
--- a/ipc/chromium/src/base/message_pump_libevent.cc
+++ b/ipc/chromium/src/base/message_pump_libevent.cc
@@ -10,17 +10,17 @@
 #include <unistd.h>
 #endif
 
 #include "eintr_wrapper.h"
 #include "base/logging.h"
 #include "base/scoped_nsautorelease_pool.h"
 #include "base/time.h"
 #include "nsDependentSubstring.h"
-#include "third_party/libevent/event.h"
+#include "event.h"
 #include "mozilla/UniquePtr.h"
 
 // This macro checks that the _EVENT_SIZEOF_* constants defined in
 // ipc/chromiume/src/third_party/<platform>/event2/event-config.h are correct.
 #define CHECK_EVENT_SIZEOF(TYPE, type) \
     static_assert(_EVENT_SIZEOF_##TYPE == sizeof(type), \
     "bad _EVENT_SIZEOF_"#TYPE);
 
deleted file mode 100644
--- a/ipc/glue/Makefile.in
+++ /dev/null
@@ -1,1 +0,0 @@
-%/SharedMemoryBasic_mach.cpp: ;
--- a/js/src/frontend/BytecodeEmitter.cpp
+++ b/js/src/frontend/BytecodeEmitter.cpp
@@ -3186,17 +3186,21 @@ BytecodeEmitter::emitSwitch(ParseNode* p
     if (switchOp == JSOP_CONDSWITCH) {
         unsigned caseNoteIndex;
         bool beforeCases = true;
         ptrdiff_t prevCaseOffset;
 
         /* Emit code for evaluating cases and jumping to case statements. */
         for (ParseNode* caseNode = cases->pn_head; caseNode; caseNode = caseNode->pn_next) {
             ParseNode* caseValue = caseNode->pn_left;
-            if (caseValue && !emitTree(caseValue))
+            // If the expression is a literal, suppress line number
+            // emission so that debugging works more naturally.
+            if (caseValue &&
+                !emitTree(caseValue, caseValue->isLiteral() ? SUPPRESS_LINENOTE :
+                          EMIT_LINENOTE))
                 return false;
             if (!beforeCases) {
                 /* prevCaseOffset is the previous JSOP_CASE's bytecode offset. */
                 if (!setSrcNoteOffset(caseNoteIndex, 0, offset() - prevCaseOffset))
                     return false;
             }
             if (!caseValue) {
                 MOZ_ASSERT(caseNode->isKind(PNK_DEFAULT));
@@ -7549,30 +7553,30 @@ BytecodeEmitter::emitClass(ParseNode* pn
     }
 
     MOZ_ALWAYS_TRUE(sc->setLocalStrictMode(savedStrictness));
 
     return true;
 }
 
 bool
-BytecodeEmitter::emitTree(ParseNode* pn)
+BytecodeEmitter::emitTree(ParseNode* pn, EmitLineNumberNote emitLineNote)
 {
     JS_CHECK_RECURSION(cx, return false);
 
     EmitLevelManager elm(this);
 
     bool ok = true;
     ptrdiff_t top = offset();
     pn->pn_offset = top;
 
     /* Emit notes to tell the current bytecode's source line number.
        However, a couple trees require special treatment; see the
        relevant emitter functions for details. */
-    if (pn->getKind() != PNK_WHILE && pn->getKind() != PNK_FOR &&
+    if (emitLineNote == EMIT_LINENOTE && pn->getKind() != PNK_WHILE && pn->getKind() != PNK_FOR &&
         !updateLineNumberNotes(pn->pn_pos.begin))
         return false;
 
     switch (pn->getKind()) {
       case PNK_FUNCTION:
         ok = emitFunction(pn);
         break;
 
--- a/js/src/frontend/BytecodeEmitter.h
+++ b/js/src/frontend/BytecodeEmitter.h
@@ -331,18 +331,24 @@ struct BytecodeEmitter
     bool addToSrcNoteDelta(jssrcnote* sn, ptrdiff_t delta);
 
     // Finish taking source notes in cx's notePool. If successful, the final
     // source note count is stored in the out outparam.
     bool finishTakingSrcNotes(uint32_t* out);
 
     void setJumpOffsetAt(ptrdiff_t off);
 
+    // Control whether emitTree emits a line number note.
+    enum EmitLineNumberNote {
+        EMIT_LINENOTE,
+        SUPPRESS_LINENOTE
+    };
+
     // Emit code for the tree rooted at pn.
-    bool emitTree(ParseNode* pn);
+    bool emitTree(ParseNode* pn, EmitLineNumberNote emitLineNote = EMIT_LINENOTE);
 
     // Emit function code for the tree rooted at body.
     bool emitFunctionScript(ParseNode* body);
 
     // Emit module code for the tree rooted at body.
     bool emitModuleScript(ParseNode* body);
 
     // Report an error if we are not processing a module.
--- a/js/src/gc/Tracer.h
+++ b/js/src/gc/Tracer.h
@@ -118,17 +118,17 @@ TraceProcessGlobalRoot(JSTracer* trc, T*
 void
 TraceGenericPointerRoot(JSTracer* trc, gc::Cell** thingp, const char* name);
 
 // Trace a non-root edge that uses the base GC thing type, instead of a more
 // specific type.
 void
 TraceManuallyBarrieredGenericPointerEdge(JSTracer* trc, gc::Cell** thingp, const char* name);
 
-// Depricated. Please use one of the strongly typed variants above.
+// Deprecated. Please use one of the strongly typed variants above.
 void
 TraceChildren(JSTracer* trc, void* thing, JS::TraceKind kind);
 
 namespace gc {
 
 // Trace through a shape or group iteratively during cycle collection to avoid
 // deep or infinite recursion.
 void
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-14.js
@@ -0,0 +1,46 @@
+// Test how stepping interacts with switch statements.
+
+var g = newGlobal();
+
+g.eval('function bob() { return "bob"; }');
+
+// Stepping into a sparse switch should not stop on literal cases.
+evaluate(`function lit(x) {     // 1
+  debugger;                     // 2
+  switch(x) {                   // 3
+  case "nope":                  // 4
+    break;                      // 5
+  case "bob":                   // 6
+    break;                      // 7
+  }                             // 8
+}`, {lineNumber: 1, global: g});
+
+// Stepping into a sparse switch should stop on non-literal cases.
+evaluate(`function nonlit(x) {  // 1
+  debugger;                     // 2
+  switch(x) {                   // 3
+  case bob():                   // 4
+    break;                      // 5
+  }                             // 6
+}`, {lineNumber: 1, global: g});
+
+var dbg = Debugger(g);
+var badStep = false;
+
+function test(s, okLine) {
+  dbg.onDebuggerStatement = function(frame) {
+    frame.onStep = function() {
+      let thisLine = this.script.getOffsetLocation(this.offset).lineNumber;
+      // The stop at line 3 is the switch.
+      if (thisLine > 3) {
+        assertEq(thisLine, okLine)
+        frame.onStep = undefined;
+      }
+    };
+  };
+  g.eval(s);
+}
+
+test("lit('bob');", 7);
+
+test("nonlit('bob');", 4);
--- a/js/src/jit/IonBuilder.cpp
+++ b/js/src/jit/IonBuilder.cpp
@@ -808,17 +808,17 @@ IonBuilder::build()
         return false;
 
 #ifdef JS_JITSPEW
     if (info().isAnalysis()) {
         JitSpew(JitSpew_IonScripts, "Analyzing script %s:%" PRIuSIZE " (%p) %s",
                 script()->filename(), script()->lineno(), (void*)script(),
                 AnalysisModeString(info().analysisMode()));
     } else {
-        JitSpew(JitSpew_IonScripts, "%sompiling script %s:%" PRIuSIZE " (%p) (warmup-counter=%" PRIuSIZE ", level=%s)",
+        JitSpew(JitSpew_IonScripts, "%sompiling script %s:%" PRIuSIZE " (%p) (warmup-counter=%" PRIu32 ", level=%s)",
                 (script()->hasIonScript() ? "Rec" : "C"),
                 script()->filename(), script()->lineno(), (void*)script(),
                 script()->getWarmUpCount(), OptimizationLevelString(optimizationInfo().level()));
     }
 #endif
 
     initParameters();
     initLocals();
--- a/js/src/jit/JitSpewer.h
+++ b/js/src/jit/JitSpewer.h
@@ -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/. */
 
 #ifndef jit_JitSpewer_h
 #define jit_JitSpewer_h
 
 #include "mozilla/DebugOnly.h"
+#include "mozilla/IntegerPrintfMacros.h"
 
 #include <stdarg.h>
 
 #include "jit/C1Spewer.h"
 #include "jit/JSONSpewer.h"
 #include "js/RootingAPI.h"
 
 namespace js {
--- a/js/src/vm/ArgumentsObject.h
+++ b/js/src/vm/ArgumentsObject.h
@@ -71,17 +71,17 @@ struct ArgumentsData
     const HeapValue* begin() const { return args; }
     HeapValue* end() { return args + numArgs; }
     const HeapValue* end() const { return args + numArgs; }
 };
 
 // Maximum supported value of arguments.length. This bounds the maximum
 // number of arguments that can be supplied to Function.prototype.apply.
 // This value also bounds the number of elements parsed in an array
-// initialiser.
+// initializer.
 static const unsigned ARGS_LENGTH_MAX = 500 * 1000;
 
 /*
  * ArgumentsObject instances represent |arguments| objects created to store
  * function arguments when a function is called.  It's expensive to create such
  * objects if they're never used, so they're only created when they are
  * potentially used.
  *
--- a/js/src/vm/PIC.h
+++ b/js/src/vm/PIC.h
@@ -161,22 +161,22 @@ struct ForOfPIC
      *      To ensure that Array.prototype has not been modified.
      *
      *  ArrayIterator.prototype (arrayIteratorProto_)
      *  ArrayIterator.prototype's shape (arrayIteratorProtoShape_)
      *      To ensure that an ArrayIterator.prototype has not been modified.
      *
      *  Array.prototype's slot number for @@iterator (arrayProtoIteratorSlot_)
      *  Array.prototype's canonical value for @@iterator (canonicalIteratorFunc_)
-     *      To quickly retreive and ensure that the iterator constructor
+     *      To quickly retrieve and ensure that the iterator constructor
      *      stored in the slot has not changed.
      *
      *  ArrayIterator.prototype's slot number for 'next' (arrayIteratorProtoNextSlot_)
      *  ArrayIterator.prototype's canonical value for 'next' (canonicalNextFunc_)
-     *      To quickly retreive and ensure that the 'next' method for ArrayIterator
+     *      To quickly retrieve and ensure that the 'next' method for ArrayIterator
      *      objects has not changed.
      */
     class Chain : public BaseChain
     {
       private:
         // Pointer to canonical Array.prototype and ArrayIterator.prototype
         HeapPtrNativeObject arrayProto_;
         HeapPtrNativeObject arrayIteratorProto_;
--- a/js/src/vm/Runtime.cpp
+++ b/js/src/vm/Runtime.cpp
@@ -612,17 +612,17 @@ InvokeInterruptCallback(JSContext* cx)
     return false;
 }
 
 void
 JSRuntime::resetJitStackLimit()
 {
     // Note that, for now, we use the untrusted limit for ion. This is fine,
     // because it's the most conservative limit, and if we hit it, we'll bail
-    // out of ion into the interpeter, which will do a proper recursion check.
+    // out of ion into the interpreter, which will do a proper recursion check.
 #ifdef JS_SIMULATOR
     jitStackLimit_ = jit::Simulator::StackLimit();
 #else
     jitStackLimit_ = mainThread.nativeStackLimit[StackForUntrustedScript];
 #endif
     jitStackLimitNoInterrupt_ = jitStackLimit_;
 }
 
--- a/js/src/vm/Runtime.h
+++ b/js/src/vm/Runtime.h
@@ -1027,17 +1027,17 @@ struct JSRuntime : public JS::shadow::Ru
      * duty (in debug builds) to verify that it matches the cx being used.
      */
     JSContext*         activeContext;
 #endif
 
     /* Garbage collector state, used by jsgc.c. */
     js::gc::GCRuntime   gc;
 
-    /* Garbage collector state has been sucessfully initialized. */
+    /* Garbage collector state has been successfully initialized. */
     bool                gcInitialized;
 
     int gcZeal() { return gc.zeal(); }
 
     void lockGC() {
         assertCanLock(js::GCLock);
         gc.lockGC();
     }
@@ -1097,17 +1097,17 @@ struct JSRuntime : public JS::shadow::Ru
     }
     void enableProfilerSampling() {
         suppressProfilerSampling = false;
     }
 
     /* Had an out-of-memory error which did not populate an exception. */
     bool                hadOutOfMemory;
 
-    /* We are curently deleting an object due to an initialization failure. */
+    /* We are currently deleting an object due to an initialization failure. */
     mozilla::DebugOnly<bool> handlingInitFailure;
 
     /* A context has been created on this runtime. */
     bool                haveCreatedContext;
 
     /*
      * Allow relazifying functions in compartments that are active. This is
      * only used by the relazifyFunctions() testing function.
--- a/js/src/vm/TraceLogging.cpp
+++ b/js/src/vm/TraceLogging.cpp
@@ -680,31 +680,35 @@ TraceLoggerThreadState::init()
     }
 
     if (ContainsFlag(env, "IonCompiler")) {
         enabledTextIds[TraceLogger_IonCompilation] = true;
         enabledTextIds[TraceLogger_IonLinking] = true;
         enabledTextIds[TraceLogger_FoldTests] = true;
         enabledTextIds[TraceLogger_SplitCriticalEdges] = true;
         enabledTextIds[TraceLogger_RenumberBlocks] = true;
+        enabledTextIds[TraceLogger_ScalarReplacement] = true;
         enabledTextIds[TraceLogger_DominatorTree] = true;
         enabledTextIds[TraceLogger_PhiAnalysis] = true;
-        enabledTextIds[TraceLogger_ScalarReplacement] = true;
+        enabledTextIds[TraceLogger_MakeLoopsContiguous] = true;
         enabledTextIds[TraceLogger_ApplyTypes] = true;
         enabledTextIds[TraceLogger_EagerSimdUnbox] = true;
         enabledTextIds[TraceLogger_AliasAnalysis] = true;
         enabledTextIds[TraceLogger_GVN] = true;
         enabledTextIds[TraceLogger_LICM] = true;
+        enabledTextIds[TraceLogger_Sincos] = true;
         enabledTextIds[TraceLogger_RangeAnalysis] = true;
         enabledTextIds[TraceLogger_LoopUnrolling] = true;
         enabledTextIds[TraceLogger_EffectiveAddressAnalysis] = true;
         enabledTextIds[TraceLogger_AlignmentMaskAnalysis] = true;
         enabledTextIds[TraceLogger_EliminateDeadCode] = true;
+        enabledTextIds[TraceLogger_ReorderInstructions] = true;
         enabledTextIds[TraceLogger_EdgeCaseAnalysis] = true;
         enabledTextIds[TraceLogger_EliminateRedundantChecks] = true;
+        enabledTextIds[TraceLogger_AddKeepAliveInstructions] = true;
         enabledTextIds[TraceLogger_GenerateLIR] = true;
         enabledTextIds[TraceLogger_RegisterAllocation] = true;
         enabledTextIds[TraceLogger_GenerateCode] = true;
         enabledTextIds[TraceLogger_Scripts] = true;
     }
 
     enabledTextIds[TraceLogger_Interpreter] = enabledTextIds[TraceLogger_Engine];
     enabledTextIds[TraceLogger_Baseline] = enabledTextIds[TraceLogger_Engine];
--- a/js/src/vm/TraceLogging.h
+++ b/js/src/vm/TraceLogging.h
@@ -30,39 +30,39 @@ class PerThreadData;
 
 namespace jit {
     class CompileRuntime;
 } // namespace jit
 
 /*
  * Tracelogging overview.
  *
- * Tracelogging makes it possible to trace the occurence of a single event and/or
+ * Tracelogging makes it possible to trace the occurrence of a single event and/or
  * the start and stop of an event. This is implemented to give an as low overhead as
  * possible so it doesn't interfere with running.
  *
  *
  * Logging something is done in 3 stages.
  * 1) Get the tracelogger of the current thread.
  *     - TraceLoggerForMainThread(JSRuntime*)
  *     - TraceLoggerForCurrentThread(); // Should NOT be used for the mainthread.
  *
  * 2) Optionally create a TraceLoggerEvent for the text that needs to get logged. This
  *    step takes some time, so try to do this beforehand, outside the hot
- *    path and don't do unnecessary repetitions, since it will criple
+ *    path and don't do unnecessary repetitions, since it will cripple
  *    performance.
  *     - TraceLoggerEvent event(logger, "foo");
  *
  *    There are also some predefined events. They are located in
  *    TraceLoggerTextId. They don't require to create an TraceLoggerEvent and
  *    can also be used as an argument to these functions.
- * 3) Log the occurence of a single event:
+ * 3) Log the occurrence of a single event:
  *    - TraceLogTimestamp(logger, TraceLoggerTextId);
  *      Note: it is temporarily not supported to provide an TraceLoggerEvent as
- *            argument to log the occurence of a single event.
+ *            argument to log the occurrence of a single event.
  *
  *    or log the start and stop of an event:
  *    - TraceLogStartEvent(logger, TraceLoggerTextId);
  *    - TraceLogStartEvent(logger, TraceLoggerEvent);
  *    - TraceLogStopEvent(logger, TraceLoggerTextId);
  *    - TraceLogStopEvent(logger, TraceLoggerEvent);
  *
  *    or the start/stop of an event with a RAII class:
--- a/js/src/vm/TraceLoggingGraph.h
+++ b/js/src/vm/TraceLoggingGraph.h
@@ -23,17 +23,17 @@
  *  - events: Name of the file containing a flat list of log events saved
  *            in binary format.
  *            (64bit: Time Stamp Counter, 32bit index to dict)
  *  - tree:   Name of the file containing the events with duration. The content
  *            is already in a tree data structure. This is also saved in a
  *            binary file.
  *  - treeFormat: The format used to encode the tree. By default "64,64,31,1,32".
  *                There are currently no other formats to save the tree.
- *     - 64,64,31,1,31 signifies how many bytes are used for the different
+ *     - 64,64,31,1,32 signifies how many bytes are used for the different
  *       parts of the tree.
  *       => 64 bits: Time Stamp Counter of start of event.
  *       => 64 bits: Time Stamp Counter of end of event.
  *       => 31 bits: Index to dict file containing the log text.
  *       =>  1 bit:  Boolean signifying if this entry has children.
  *                   When true, the child can be found just right after this entry.
  *       => 32 bits: Containing the ID of the next event on the same depth
  *                   or 0 if there isn't an event on the same depth anymore.
--- a/js/src/vm/TypedArrayObject.cpp
+++ b/js/src/vm/TypedArrayObject.cpp
@@ -1826,17 +1826,17 @@ const Class TypedArrayObject::classes[Sc
 // Thus we need one class with cached prototype per kind of typed array, with a
 // delegated ClassSpec.
 #define IMPL_TYPED_ARRAY_PROTO_CLASS(typedArray, i) \
 { \
     /*
      * Actually ({}).toString.call(Uint8Array.prototype) should throw, because
      * Uint8Array.prototype lacks the the typed array internal slots.  (Same as
      * with %TypedArray%.prototype.)  It's not clear this is desirable (see
-     * above), but it's what we've always done, so keep doing it til we
+     * above), but it's what we've always done, so keep doing it till we
      * implement @@toStringTag or ES6 changes.
      */ \
     #typedArray "Prototype", \
     JSCLASS_HAS_CACHED_PROTO(JSProto_##typedArray), \
     nullptr, /* addProperty */ \
     nullptr, /* delProperty */ \
     nullptr, /* getProperty */ \
     nullptr, /* setProperty */ \
--- a/layout/base/FrameLayerBuilder.cpp
+++ b/layout/base/FrameLayerBuilder.cpp
@@ -1056,17 +1056,17 @@ public:
     mAppUnitsPerDevPixel = presContext->AppUnitsPerDevPixel();
     mContainerReferenceFrame =
       const_cast<nsIFrame*>(aContainerItem ? aContainerItem->ReferenceFrameForChildren() :
                                              mBuilder->FindReferenceFrameFor(mContainerFrame));
     bool isAtRoot = !aContainerItem || (aContainerItem->Frame() == mBuilder->RootReferenceFrame());
     MOZ_ASSERT_IF(isAtRoot, mContainerReferenceFrame == mBuilder->RootReferenceFrame());
     mContainerAnimatedGeometryRoot = isAtRoot
       ? mContainerReferenceFrame
-      : nsLayoutUtils::GetAnimatedGeometryRootFor(aContainerItem, aBuilder);
+      : aContainerItem->AnimatedGeometryRoot();
     MOZ_ASSERT(!mBuilder->IsPaintingToWindow() ||
       nsLayoutUtils::IsAncestorFrameCrossDoc(mBuilder->RootReferenceFrame(),
                                              mContainerAnimatedGeometryRoot));
     NS_ASSERTION(!aContainerItem || !aContainerItem->ShouldFixToViewport(mBuilder),
                  "Container items never return true for ShouldFixToViewport");
     mContainerFixedPosFrame =
         FindFixedPosFrameForLayerData(mContainerAnimatedGeometryRoot, false);
     // When AllowResidualTranslation is false, display items will be drawn
@@ -3711,18 +3711,17 @@ ContainerState::ChooseAnimatedGeometryRo
     // Don't use an item that won't be part of any PaintedLayers to pick the
     // active scrolled root.
     if (layerState == LAYER_ACTIVE_FORCE) {
       continue;
     }
 
     // Try using the actual active scrolled root of the backmost item, as that
     // should result in the least invalidation when scrolling.
-    *aAnimatedGeometryRoot =
-      nsLayoutUtils::GetAnimatedGeometryRootFor(item, mBuilder);
+    *aAnimatedGeometryRoot = item->AnimatedGeometryRoot();
     return true;
   }
   return false;
 }
 
 nsIntRegion
 ContainerState::ComputeOpaqueRect(nsDisplayItem* aItem,
                                   const nsIFrame* aAnimatedGeometryRoot,
@@ -3887,18 +3886,17 @@ ContainerState::ProcessDisplayItems(nsDi
     if (layerState == LAYER_INACTIVE &&
         nsDisplayItem::ForceActiveLayers()) {
       layerState = LAYER_ACTIVE;
     }
 
     bool forceInactive;
     const nsIFrame* animatedGeometryRoot;
     const nsIFrame* animatedGeometryRootForScrollMetadata = nullptr;
-    const nsIFrame* realAnimatedGeometryRootOfItem =
-      nsLayoutUtils::GetAnimatedGeometryRootFor(item, mBuilder);
+    const nsIFrame* realAnimatedGeometryRootOfItem = item->AnimatedGeometryRoot();
     if (mFlattenToSingleLayer) {
       forceInactive = true;
       animatedGeometryRoot = lastAnimatedGeometryRoot;
     } else {
       forceInactive = false;
       if (mManager->IsWidgetLayerManager()) {
         animatedGeometryRoot = realAnimatedGeometryRootOfItem;
         // Unlike GetAnimatedGeometryRootFor(), GetAnimatedGeometryRootForFrame() does not
--- a/layout/base/nsDisplayList.cpp
+++ b/layout/base/nsDisplayList.cpp
@@ -1991,21 +1991,27 @@ void nsDisplayList::SortByCSSOrder(nsDis
 void nsDisplayList::Sort(nsDisplayListBuilder* aBuilder,
                          SortLEQ aCmp, void* aClosure) {
   ::Sort(this, Count(), aCmp, aClosure);
 }
 
 nsDisplayItem::nsDisplayItem(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame)
   : mFrame(aFrame)
   , mClip(aBuilder->ClipState().GetCurrentCombinedClip(aBuilder))
+  , mAnimatedGeometryRoot(nullptr)
 #ifdef MOZ_DUMP_PAINTING
   , mPainted(false)
 #endif
 {
   mReferenceFrame = aBuilder->FindReferenceFrameFor(aFrame, &mToReferenceFrame);
+  // This can return the wrong result if the item override ShouldFixToViewport(),
+  // the item needs to set it again in its constructor.
+  mAnimatedGeometryRoot = nsLayoutUtils::GetAnimatedGeometryRootForInit(this, aBuilder);
+  MOZ_ASSERT(nsLayoutUtils::IsAncestorFrameCrossDoc(aBuilder->RootReferenceFrame(),
+                                                      mAnimatedGeometryRoot), "Bad");
   NS_ASSERTION(aBuilder->GetDirtyRect().width >= 0 ||
                !aBuilder->IsForPainting(), "dirty rect not set");
   // The dirty rect is for mCurrentFrame, so we have to use
   // mCurrentOffsetToReferenceFrame
   mVisibleRect = aBuilder->GetDirtyRect() +
       aBuilder->GetCurrentFrameOffsetToReferenceFrame();
 }
 
@@ -2136,16 +2142,19 @@ nsDisplayBackgroundImage::nsDisplayBackg
   : nsDisplayImageContainer(aBuilder, aFrame)
   , mBackgroundStyle(aBackgroundStyle)
   , mLayer(aLayer)
 {
   MOZ_COUNT_CTOR(nsDisplayBackgroundImage);
 
   mBounds = GetBoundsInternal(aBuilder);
   mDestArea = GetDestAreaInternal(aBuilder);
+  if (ShouldFixToViewport(aBuilder)) {
+    mAnimatedGeometryRoot = nsLayoutUtils::GetAnimatedGeometryRootFor(this, aBuilder);
+  }
 }
 
 nsRect
 nsDisplayBackgroundImage::GetDestAreaInternal(nsDisplayListBuilder* aBuilder)
 {
   if (!mBackgroundStyle) {
     return nsRect();
   }
@@ -3781,18 +3790,17 @@ RequiredLayerStateForChildren(nsDisplayL
                               LayerManager* aManager,
                               const ContainerLayerParameters& aParameters,
                               const nsDisplayList& aList,
                               nsIFrame* aExpectedAnimatedGeometryRootForChildren)
 {
   LayerState result = LAYER_INACTIVE;
   for (nsDisplayItem* i = aList.GetBottom(); i; i = i->GetAbove()) {
     if (result == LAYER_INACTIVE &&
-        nsLayoutUtils::GetAnimatedGeometryRootFor(i, aBuilder) !=
-          aExpectedAnimatedGeometryRootForChildren) {
+        i->AnimatedGeometryRoot() != aExpectedAnimatedGeometryRootForChildren) {
       result = LAYER_ACTIVE;
     }
 
     LayerState state = i->GetLayerState(aBuilder, aManager, aParameters);
     if ((state == LAYER_ACTIVE || state == LAYER_ACTIVE_FORCE) &&
         state > result) {
       result = state;
     }
@@ -4056,18 +4064,17 @@ nsDisplayOpacity::GetLayerState(nsDispla
   if (mForEventsOnly) {
     MOZ_ASSERT(mOpacity == 0);
     return LAYER_INACTIVE;
   }
 
   if (NeedsActiveLayer(aBuilder))
     return LAYER_ACTIVE;
 
-  return RequiredLayerStateForChildren(aBuilder, aManager, aParameters, mList,
-    nsLayoutUtils::GetAnimatedGeometryRootFor(this, aBuilder));
+  return RequiredLayerStateForChildren(aBuilder, aManager, aParameters, mList, AnimatedGeometryRoot());
 }
 
 bool
 nsDisplayOpacity::ComputeVisibility(nsDisplayListBuilder* aBuilder,
                                     nsRegion* aVisibleRegion) {
   // Our children are translucent so we should not allow them to subtract
   // area from aVisibleRegion. We do need to find out what is visible under
   // our children in the temporary compositing buffer, because if our children
@@ -4789,16 +4796,17 @@ nsDisplayTransform::SetReferenceFrameToA
 {
   if (mFrame == aBuilder->RootReferenceFrame()) {
     return;
   }
   nsIFrame *outerFrame = nsLayoutUtils::GetCrossDocParentFrame(mFrame);
   mReferenceFrame =
     aBuilder->FindReferenceFrameFor(outerFrame);
   mToReferenceFrame = mFrame->GetOffsetToCrossDoc(mReferenceFrame);
+  mAnimatedGeometryRoot = nsLayoutUtils::GetAnimatedGeometryRootForFrame(aBuilder, outerFrame);
   mVisibleRect = aBuilder->GetDirtyRect() + mToReferenceFrame;
 }
 
 void
 nsDisplayTransform::Init(nsDisplayListBuilder* aBuilder)
 {
   mHasBounds = false;
   mStoredList.SetClip(aBuilder, DisplayItemClip::NoClip());
--- a/layout/base/nsDisplayList.h
+++ b/layout/base/nsDisplayList.h
@@ -1226,16 +1226,17 @@ public:
   /**
    * This constructor is only used in rare cases when we need to construct
    * temporary items.
    */
   explicit nsDisplayItem(nsIFrame* aFrame)
     : mFrame(aFrame)
     , mClip(nullptr)
     , mReferenceFrame(nullptr)
+    , mAnimatedGeometryRoot(nullptr)
 #ifdef MOZ_DUMP_PAINTING
     , mPainted(false)
 #endif
   {
   }
   virtual ~nsDisplayItem() {}
 
   void* operator new(size_t aSize,
@@ -1687,16 +1688,21 @@ public:
    */
   const nsIFrame* ReferenceFrame() const { return mReferenceFrame; }
 
   /**
    * Returns the reference frame for display item children of this item.
    */
   virtual const nsIFrame* ReferenceFrameForChildren() const { return mReferenceFrame; }
 
+  nsIFrame* AnimatedGeometryRoot() const {
+    MOZ_ASSERT(mAnimatedGeometryRoot, "Must have cached AGR before accessing it!");
+    return mAnimatedGeometryRoot;
+  }
+
   /**
    * Checks if this display item (or any children) contains content that might
    * be rendered with component alpha (e.g. subpixel antialiasing). Returns the
    * bounds of the area that needs component alpha, or an empty rect if nothing
    * in the item does.
    */
   virtual nsRect GetComponentAlphaBounds(nsDisplayListBuilder* aBuilder) { return nsRect(); }
 
@@ -1746,16 +1752,17 @@ protected:
   friend class nsDisplayList;
 
   nsDisplayItem() { mAbove = nullptr; }
 
   nsIFrame* mFrame;
   const DisplayItemClip* mClip;
   // Result of FindReferenceFrameFor(mFrame), if mFrame is non-null
   const nsIFrame* mReferenceFrame;
+  nsIFrame* mAnimatedGeometryRoot;
   // Result of ToReferenceFrame(mFrame), if mFrame is non-null
   nsPoint   mToReferenceFrame;
   // This is the rectangle that needs to be painted.
   // Display item construction sets this to the dirty rect.
   // nsDisplayList::ComputeVisibility sets this to the visible region
   // of the item by intersecting the current visible region with the bounds
   // of the item. Paint implementations can use this to limit their drawing.
   // Guaranteed to be contained in GetBounds().
--- a/layout/base/nsLayoutDebugger.cpp
+++ b/layout/base/nsLayoutDebugger.cpp
@@ -157,42 +157,41 @@ PrintDisplayItemTo(nsDisplayListBuilder*
     if (content->GetClasses()) {
       content->GetClasses()->ToString(tmp);
       contentData.AppendLiteral(" class:");
       contentData.Append(tmp);
     }
   }
   bool snap;
   nsRect rect = aItem->GetBounds(aBuilder, &snap);
-  nsRect layerRect = rect -
-    nsLayoutUtils::GetAnimatedGeometryRootFor(aItem, aBuilder)->
-      GetOffsetToCrossDoc(aItem->ReferenceFrame());
+  nsRect layerRect = rect - aItem->AnimatedGeometryRoot()->GetOffsetToCrossDoc(aItem->ReferenceFrame());
   nscolor color;
   nsRect vis = aItem->GetVisibleRect();
   nsRect component = aItem->GetComponentAlphaBounds(aBuilder);
   nsDisplayList* list = aItem->GetChildren();
   const DisplayItemClip& clip = aItem->GetClip();
   nsRegion opaque = aItem->GetOpaqueRegion(aBuilder, &snap);
 #ifdef MOZ_DUMP_PAINTING
   if (aDumpHtml && aItem->Painted()) {
     nsCString string(aItem->Name());
     string.Append('-');
     string.AppendInt((uint64_t)aItem);
     aStream << nsPrintfCString("<a href=\"javascript:ViewImage('%s')\">", string.BeginReading());
   }
 #endif
-  aStream << nsPrintfCString("%s p=0x%p f=0x%p(%s) %sbounds(%d,%d,%d,%d) layerBounds(%d,%d,%d,%d) visible(%d,%d,%d,%d) componentAlpha(%d,%d,%d,%d) clip(%s) %s",
+  aStream << nsPrintfCString("%s p=0x%p f=0x%p(%s) %sbounds(%d,%d,%d,%d) layerBounds(%d,%d,%d,%d) visible(%d,%d,%d,%d) componentAlpha(%d,%d,%d,%d) clip(%s) %s ref=0x%p agr=0x%p",
           aItem->Name(), aItem, (void*)f, NS_ConvertUTF16toUTF8(contentData).get(),
           (aItem->ZIndex() ? nsPrintfCString("z=%d ", aItem->ZIndex()).get() : ""),
           rect.x, rect.y, rect.width, rect.height,
           layerRect.x, layerRect.y, layerRect.width, layerRect.height,
           vis.x, vis.y, vis.width, vis.height,
           component.x, component.y, component.width, component.height,
           clip.ToString().get(),
-          aItem->IsUniform(aBuilder, &color) ? " uniform" : "");
+          aItem->IsUniform(aBuilder, &color) ? " uniform" : "",
+          aItem->ReferenceFrame(), aItem->AnimatedGeometryRoot());
 
   nsRegionRectIterator iter(opaque);
   for (const nsRect* r = iter.Next(); r; r = iter.Next()) {
     aStream << nsPrintfCString(" (opaque %d,%d,%d,%d)", r->x, r->y, r->width, r->height);
   }
 
   if (aItem->ShouldFixToViewport(aBuilder)) {
     aStream << " fixed";
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -1062,22 +1062,16 @@ GetDisplayPortFromMarginsData(nsIContent
 
       float left = std::min(margins.left, float(budget));
       float right = std::min(margins.right, budget - left);
       screenRect.x -= left;
       screenRect.width += left + right;
     }
   }
 
-  // Inflate the rectangle by 1 so that we always push to the next tile
-  // boundary. This is desirable to stop from having a rectangle with a
-  // moving origin occasionally being smaller when it coincidentally lines
-  // up to tile boundaries.
-  screenRect.Inflate(1);
-
   ScreenPoint scrollPosScreen = LayoutDevicePoint::FromAppUnits(scrollPos, auPerDevPixel)
                               * res;
 
   // Round-out the display port to the nearest alignment (tiles)
   screenRect += scrollPosScreen;
   float x = alignment.width * floor(screenRect.x / alignment.width);
   float y = alignment.height * floor(screenRect.y / alignment.height);
   float w = alignment.width * ceil(screenRect.width / alignment.width + 1);
@@ -1956,16 +1950,35 @@ nsLayoutUtils::GetAnimatedGeometryRootFo
     nsIFrame* parent = nsLayoutUtils::GetCrossDocParentFrame(f);
     if (parent) {
       return GetAnimatedGeometryRootForFrame(aBuilder, parent);
     }
   }
   return GetAnimatedGeometryRootForFrame(aBuilder, f);
 }
 
+nsIFrame*
+nsLayoutUtils::GetAnimatedGeometryRootForInit(nsDisplayItem* aItem,
+                                              nsDisplayListBuilder* aBuilder)
+{
+  nsIFrame* f = aItem->Frame();
+  if (aItem->ShouldFixToViewport(aBuilder)) {
+    // Make its active scrolled root be the active scrolled root of
+    // the enclosing viewport, since it shouldn't be scrolled by scrolled
+    // frames in its document. InvalidateFixedBackgroundFramesFromList in
+    // nsGfxScrollFrame will not repaint this item when scrolling occurs.
+    nsIFrame* viewportFrame =
+      nsLayoutUtils::GetClosestFrameOfType(f, nsGkAtoms::viewportFrame, aBuilder->RootReferenceFrame());
+    if (viewportFrame) {
+      return GetAnimatedGeometryRootForFrame(aBuilder, viewportFrame);
+    }
+  }
+  return GetAnimatedGeometryRootForFrame(aBuilder, f);
+}
+
 // static
 nsIScrollableFrame*
 nsLayoutUtils::GetNearestScrollableFrameForDirection(nsIFrame* aFrame,
                                                      Direction aDirection)
 {
   NS_ASSERTION(aFrame, "GetNearestScrollableFrameForDirection expects a non-null frame");
   for (nsIFrame* f = aFrame; f; f = nsLayoutUtils::GetCrossDocParentFrame(f)) {
     nsIScrollableFrame* scrollableFrame = do_QueryFrame(f);
--- a/layout/base/nsLayoutUtils.h
+++ b/layout/base/nsLayoutUtils.h
@@ -558,16 +558,23 @@ public:
      * do not do any special processing for background attachment fixed items,
      * instead treating them like any other frame.
      */
     AGR_IGNORE_BACKGROUND_ATTACHMENT_FIXED = 0x01
   };
   static nsIFrame* GetAnimatedGeometryRootFor(nsDisplayItem* aItem,
                                               nsDisplayListBuilder* aBuilder,
                                               uint32_t aFlags = 0);
+  /**
+   * Version of GetAnimatedGeometryRootFor that can be called in nsDisplayItem
+   * constructor, and only from there. Not valid for transform items, but they
+   * set their AGR in their constructor.
+   */
+  static nsIFrame* GetAnimatedGeometryRootForInit(nsDisplayItem* aItem,
+                                                  nsDisplayListBuilder* aBuilder);
 
   /**
    * Finds the nearest ancestor frame to aFrame that is considered to have (or
    * will have) "animated geometry". This could be aFrame.
    */
   static nsIFrame* GetAnimatedGeometryRootForFrame(nsDisplayListBuilder* aBuilder,
                                                    nsIFrame* aFrame);
 
--- a/layout/generic/nsCanvasFrame.h
+++ b/layout/generic/nsCanvasFrame.h
@@ -234,17 +234,21 @@ public:
 #endif
 };
 
 class nsDisplayCanvasBackgroundImage : public nsDisplayBackgroundImage {
 public:
   nsDisplayCanvasBackgroundImage(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame,
                                  uint32_t aLayer, const nsStyleBackground* aBg)
     : nsDisplayBackgroundImage(aBuilder, aFrame, aLayer, aBg)
-  {}
+  {
+    if (ShouldFixToViewport(aBuilder)) {
+      mAnimatedGeometryRoot = nsLayoutUtils::GetAnimatedGeometryRootFor(this, aBuilder);
+    }
+  }
 
   virtual void Paint(nsDisplayListBuilder* aBuilder, nsRenderingContext* aCtx) override;
 
   virtual void NotifyRenderingChanged() override
   {
     mFrame->Properties().Delete(nsIFrame::CachedBackgroundImage());
     mFrame->Properties().Delete(nsIFrame::CachedBackgroundImageDT());
   }
--- a/layout/generic/nsGfxScrollFrame.cpp
+++ b/layout/generic/nsGfxScrollFrame.cpp
@@ -4747,35 +4747,41 @@ ScrollFrameHelper::FinishReflowForScroll
   SetCoordAttribute(aContent, nsGkAtoms::maxpos, aMaxXY - aMinXY);
   SetCoordAttribute(aContent, nsGkAtoms::pageincrement, aPageIncrement);
   SetCoordAttribute(aContent, nsGkAtoms::increment, aIncrement);
 }
 
 bool
 ScrollFrameHelper::ReflowFinished()
 {
+  mPostedReflowCallback = false;
+
+  if (NS_SUBTREE_DIRTY(mOuter)) {
+    // We will get another call after the next reflow and scrolling
+    // later is less janky.
+    return false;
+  }
+
   nsAutoScriptBlocker scriptBlocker;
-  mPostedReflowCallback = false;
-
   ScrollToRestoredPosition();
 
   // Clamp current scroll position to new bounds. Normally this won't
   // do anything.
   nsPoint currentScrollPos = GetScrollPosition();
   ScrollToImpl(currentScrollPos, nsRect(currentScrollPos, nsSize(0, 0)));
   if (!mAsyncScroll && !mAsyncSmoothMSDScroll) {
     // We need to have mDestination track the current scroll position,
     // in case it falls outside the new reflow area. mDestination is used
     // by ScrollBy as its starting position.
     mDestination = GetScrollPosition();
   }
 
-  if (NS_SUBTREE_DIRTY(mOuter) || !mUpdateScrollbarAttributes)
+  if (!mUpdateScrollbarAttributes) {
     return false;
-
+  }
   mUpdateScrollbarAttributes = false;
 
   // Update scrollbar attributes.
   nsPresContext* presContext = mOuter->PresContext();
 
   if (mMayHaveDirtyFixedChildren) {
     mMayHaveDirtyFixedChildren = false;
     nsIFrame* parentFrame = mOuter->GetParent();
--- a/media/libstagefright/binding/MP4Metadata.cpp
+++ b/media/libstagefright/binding/MP4Metadata.cpp
@@ -97,17 +97,19 @@ MP4Metadata::MP4Metadata(Stream* aSource
   , mSource(aSource)
 {
   mPrivate->mMetadataExtractor =
     new MPEG4Extractor(new DataSourceAdapter(mSource));
   mPrivate->mCanSeek =
     mPrivate->mMetadataExtractor->flags() & MediaExtractor::CAN_SEEK;
   sp<MetaData> metaData = mPrivate->mMetadataExtractor->getMetaData();
 
-  UpdateCrypto(metaData.get());
+  if (metaData.get()) {
+    UpdateCrypto(metaData.get());
+  }
 }
 
 MP4Metadata::~MP4Metadata()
 {
 }
 
 #ifdef MOZ_RUST_MP4PARSE
 #include "mp4parse.h"
@@ -312,16 +314,19 @@ MP4Metadata::ReadTrackIndex(FallibleTArr
 }
 
 int32_t
 MP4Metadata::GetTrackNumber(mozilla::TrackID aTrackID)
 {
   size_t numTracks = mPrivate->mMetadataExtractor->countTracks();
   for (size_t i = 0; i < numTracks; i++) {
     sp<MetaData> metaData = mPrivate->mMetadataExtractor->getTrackMetaData(i);
+    if (!metaData.get()) {
+      continue;
+    }
     int32_t value;
     if (metaData->findInt32(kKeyTrackID, &value) && value == aTrackID) {
       return i;
     }
   }
   return -1;
 }
 
--- a/media/libstagefright/frameworks/av/include/media/stagefright/MetaData.h
+++ b/media/libstagefright/frameworks/av/include/media/stagefright/MetaData.h
@@ -243,17 +243,17 @@ private:
             void *ext_data;
             float reservoir;
         } u;
 
         bool usesReservoir() const {
             return mSize <= sizeof(u.reservoir);
         }
 
-        void allocateStorage(size_t size);
+        bool allocateStorage(size_t size);
         void freeStorage();
 
         void *storage() {
             return usesReservoir() ? &u.reservoir : u.ext_data;
         }
 
         const void *storage() const {
             return usesReservoir() ? &u.reservoir : u.ext_data;
--- a/media/libstagefright/frameworks/av/media/libstagefright/MPEG4Extractor.cpp
+++ b/media/libstagefright/frameworks/av/media/libstagefright/MPEG4Extractor.cpp
@@ -415,17 +415,17 @@ MPEG4Extractor::~MPEG4Extractor() {
 
 uint32_t MPEG4Extractor::flags() const {
     return CAN_PAUSE | CAN_SEEK_BACKWARD | CAN_SEEK_FORWARD | CAN_SEEK;
 }
 
 sp<MetaData> MPEG4Extractor::getMetaData() {
     status_t err;
     if ((err = readMetaData()) != OK) {
-        return new MetaData;
+        return NULL;
     }
 
     return mFileMetaData;
 }
 
 size_t MPEG4Extractor::countTracks() {
     status_t err;
     if ((err = readMetaData()) != OK) {
@@ -509,24 +509,25 @@ status_t MPEG4Extractor::readMetaData() 
     }
 
     CHECK_NE(err, (status_t)NO_INIT);
 
     // copy pssh data into file metadata
     uint64_t psshsize = 0;
     for (size_t i = 0; i < mPssh.Length(); i++) {
         psshsize += 20 + mPssh[i].datalen;
-    }
-    if (psshsize > kMAX_ALLOCATION) {
-        return ERROR_MALFORMED;
+        if (mPssh[i].datalen > kMAX_ALLOCATION - 20 ||
+            psshsize > kMAX_ALLOCATION) {
+            return ERROR_MALFORMED;
+        }
     }
     if (psshsize) {
         char *buf = (char*)malloc(psshsize);
         if (!buf) {
-            return ERROR_MALFORMED;
+            return -ENOMEM;
         }
         char *ptr = buf;
         for (size_t i = 0; i < mPssh.Length(); i++) {
             memcpy(ptr, mPssh[i].uuid, 20); // uuid + length
             memcpy(ptr + 20, mPssh[i].data, mPssh[i].datalen);
             ptr += (20 + mPssh[i].datalen);
         }
         mFileMetaData->setData(kKeyPssh, 'pssh', buf, psshsize);
@@ -680,17 +681,20 @@ status_t MPEG4Extractor::parseDrmSINF(of
             SINF *sinf = mFirstSINF;
             while (sinf && (sinf->IPMPDescriptorID != id)) {
                 sinf = sinf->next;
             }
             if (sinf == NULL) {
                 return ERROR_MALFORMED;
             }
             sinf->len = dataLen - 3;
-            sinf->IPMPData = new char[sinf->len];
+            sinf->IPMPData = new (fallible) char[sinf->len];
+            if (!sinf->IPMPData) {
+                return -ENOMEM;
+            }
 
             if (mDataSource->readAt(data_offset + 2, sinf->IPMPData, sinf->len) < sinf->len) {
                 return ERROR_IO;
             }
             data_offset += sinf->len;
 
             size -= (dataLen + numOfBytes + 1);
         }
@@ -1141,17 +1145,20 @@ status_t MPEG4Extractor::parseChunk(off6
             }
 
             if (mDataSource->readAt(data_offset + 4, &pssh.uuid, 16) < 16) {
                 return ERROR_IO;
             }
 
             // Copy the contents of the box (including header) verbatim.
             pssh.datalen = chunk_data_size + 8;
-            pssh.data = new uint8_t[pssh.datalen];
+            pssh.data = new (fallible) uint8_t[pssh.datalen];
+            if (!pssh.data) {
+                return -ENOMEM;
+            }
             if (mDataSource->readAt(data_offset - 8, pssh.data, pssh.datalen) < pssh.datalen) {
                 return ERROR_IO;
             }
 
             mPssh.AppendElement(pssh);
 
             *offset += chunk_size;
             break;
@@ -1757,17 +1764,20 @@ status_t MPEG4Extractor::parseChunk(off6
 
         case FOURCC('a', 'v', 'c', 'C'):
         {
             if (chunk_data_size < 7) {
               ALOGE("short avcC chunk (%d bytes)", chunk_data_size);
               return ERROR_MALFORMED;
             }
 
-            sp<ABuffer> buffer = new ABuffer(chunk_data_size);
+            sp<ABuffer> buffer = new (fallible) ABuffer(chunk_data_size);
+            if (!buffer.get()) {
+                return -ENOMEM;
+            }
 
             if (mDataSource->readAt(
                         data_offset, buffer->data(), chunk_data_size) < chunk_data_size) {
                 return ERROR_IO;
             }
 
             if (!mLastTrack) {
               return ERROR_MALFORMED;
@@ -1991,17 +2001,20 @@ status_t MPEG4Extractor::parseChunk(off6
                     kKeyTextFormatData, &type, &data, &size)) {
                 size = 0;
             }
 
             // Make sure (size + chunk_size) isn't going to overflow.
             if (size >= kMAX_ALLOCATION - chunk_size) {
                 return ERROR_MALFORMED;
             }
-            uint8_t *buffer = new uint8_t[size + chunk_size];
+            uint8_t *buffer = new (fallible) uint8_t[size + chunk_size];
+            if (!buffer) {
+                return -ENOMEM;
+            }
 
             if (size > 0) {
                 memcpy(buffer, data, size);
             }
 
             if ((size_t)(mDataSource->readAt(*offset, buffer + size, chunk_size))
                     < chunk_size) {
                 delete[] buffer;
@@ -2019,22 +2032,28 @@ status_t MPEG4Extractor::parseChunk(off6
             break;
         }
 
         case FOURCC('c', 'o', 'v', 'r'):
         {
             if (mFileMetaData != NULL) {
                 ALOGV("chunk_data_size = %lld and data_offset = %lld",
                         chunk_data_size, data_offset);
-                sp<ABuffer> buffer = new ABuffer(chunk_data_size + 1);
+                const int kSkipBytesOfDataBox = 16;
+                if (chunk_data_size <= kSkipBytesOfDataBox) {
+                  return ERROR_MALFORMED;
+                }
+                sp<ABuffer> buffer = new (fallible) ABuffer(chunk_data_size + 1);
+                if (!buffer.get()) {
+                    return -ENOMEM;
+                }
                 if (mDataSource->readAt(
                     data_offset, buffer->data(), chunk_data_size) != (ssize_t)chunk_data_size) {
                     return ERROR_IO;
                 }
-                const int kSkipBytesOfDataBox = 16;
                 mFileMetaData->setData(
                     kKeyAlbumArt, MetaData::TYPE_NONE,
                     buffer->data() + kSkipBytesOfDataBox, chunk_data_size - kSkipBytesOfDataBox);
             }
 
             *offset += chunk_size;
             break;
         }
--- a/media/libstagefright/frameworks/av/media/libstagefright/MetaData.cpp
+++ b/media/libstagefright/frameworks/av/media/libstagefright/MetaData.cpp
@@ -215,73 +215,84 @@ bool MetaData::findData(uint32_t key, ui
     const typed_data &item = mItems.valueAt(i);
 
     item.getData(type, data, size);
 
     return true;
 }
 
 MetaData::typed_data::typed_data()
-    : mType(0),
+    : mType(TYPE_NONE),
       mSize(0) {
 }
 
 MetaData::typed_data::~typed_data() {
     clear();
 }
 
 MetaData::typed_data::typed_data(const typed_data &from)
     : mType(from.mType),
       mSize(0) {
-    allocateStorage(from.mSize);
-    memcpy(storage(), from.storage(), mSize);
+    if (allocateStorage(from.mSize)) {
+        memcpy(storage(), from.storage(), mSize);
+    }
 }
 
 MetaData::typed_data &MetaData::typed_data::operator=(
         const MetaData::typed_data &from) {
     if (this != &from) {
         clear();
-        mType = from.mType;
-        allocateStorage(from.mSize);
-        memcpy(storage(), from.storage(), mSize);
+        if (allocateStorage(from.mSize)) {
+            mType = from.mType;
+            memcpy(storage(), from.storage(), mSize);
+        }
     }
 
     return *this;
 }
 
 void MetaData::typed_data::clear() {
     freeStorage();
 
-    mType = 0;
+    mType = TYPE_NONE;
 }
 
 void MetaData::typed_data::setData(
         uint32_t type, const void *data, size_t size) {
     clear();
 
-    mType = type;
-    allocateStorage(size);
-    memcpy(storage(), data, size);
+    if (allocateStorage(size)) {
+        mType = type;
+        memcpy(storage(), data, size);
+    }
 }
 
 void MetaData::typed_data::getData(
         uint32_t *type, const void **data, size_t *size) const {
     *type = mType;
     *size = mSize;
     *data = storage();
 }
 
-void MetaData::typed_data::allocateStorage(size_t size) {
+bool MetaData::typed_data::allocateStorage(size_t size) {
+    // Update mSize now, as it is needed by usesReservoir() below.
+    // (mSize will be reset if the allocation fails further below.)
     mSize = size;
 
     if (usesReservoir()) {
-        return;
+        return true;
     }
 
     u.ext_data = malloc(mSize);
+    if (!u.ext_data) {
+      mType = TYPE_NONE;
+      mSize = 0;
+      return false;
+    }
+    return true;
 }
 
 void MetaData::typed_data::freeStorage() {
     if (!usesReservoir()) {
         if (u.ext_data) {
             free(u.ext_data);
             u.ext_data = NULL;
         }
--- a/media/libstagefright/gtest/TestParser.cpp
+++ b/media/libstagefright/gtest/TestParser.cpp
@@ -169,17 +169,18 @@ static const TestFileData testFiles[] = 
   { "test_case_1181213.mp4", 0,   0,   0, 0 },
   { "test_case_1181215.mp4", 0,   0,   0, 0 },
   { "test_case_1181220.mp4", 0,   0,   0, 0 },
   { "test_case_1181223.mp4", 0,   0,   0, 0 },
   { "test_case_1181719.mp4", 0,   0,   0, 0 },
   { "test_case_1185230.mp4", 1, 320, 240, 1 },
   { "test_case_1187067.mp4", 1, 160,  90, 0 },
   { "test_case_1200326.mp4", 0,   0,   0, 0 },
-  { "test_case_1204580.mp4", 1, 320, 180, 0 }
+  { "test_case_1204580.mp4", 1, 320, 180, 0 },
+  { "test_case_1216748.mp4", 0,   0,   0, 0 }
 };
 
 TEST(stagefright_MPEG4Metadata, test_case_mp4)
 {
   for (size_t test = 0; test < ArrayLength(testFiles); ++test) {
     nsTArray<uint8_t> buffer = ReadTestFile(testFiles[test].mFilename);
     ASSERT_FALSE(buffer.IsEmpty());
     RefPtr<Stream> stream = new TestStream(buffer.Elements(), buffer.Length());
--- a/media/libstagefright/gtest/moz.build
+++ b/media/libstagefright/gtest/moz.build
@@ -17,16 +17,17 @@ TEST_HARNESS_FILES.gtest += [
     'test_case_1181215.mp4',
     'test_case_1181220.mp4',
     'test_case_1181223.mp4',
     'test_case_1181719.mp4',
     'test_case_1185230.mp4',
     'test_case_1187067.mp4',
     'test_case_1200326.mp4',
     'test_case_1204580.mp4',
+    'test_case_1216748.mp4',
 ]
 
 if CONFIG['MOZ_RUST']:
     UNIFIED_SOURCES += ['TestMP4Rust.cpp',]
     TEST_HARNESS_FILES.gtest += [
         '../../../dom/media/test/street.mp4',
     ]
     LOCAL_INCLUDES += [
new file mode 100755
index 0000000000000000000000000000000000000000..7072f53bec645e38947b1b2322c4f0704d9d31d5
GIT binary patch
literal 296
zc${NkV30^FsVvAXFfn3aU|<B%Kx_zPH{|B$mjUUV+_H=m1{A=(m4U%z4+A3u2q6F?
zsw#B80|Q>b1jH>RMTyx!IwCp0tO#VnSqs}15Js2=v=<#P;$TDVWDR3rV1{s2Qd3Hb
efwTzJe5ekvnH*3tj<llGR5&df|2{V*u>=6D*&+`B
--- a/media/mtransport/third_party/nICEr/src/stun/stun_client_ctx.c
+++ b/media/mtransport/third_party/nICEr/src/stun/stun_client_ctx.c
@@ -68,43 +68,44 @@ int nr_stun_client_ctx_create(char *labe
     if(!(ctx->label=r_strdup(label)))
       ABORT(R_NO_MEMORY);
 
     ctx->sock=sock;
 
     nr_socket_getaddr(sock,&ctx->my_addr);
     nr_transport_addr_copy(&ctx->peer_addr,peer);
 
-    if (NR_reg_get_uint4(NR_STUN_REG_PREF_CLNT_RETRANSMIT_TIMEOUT, &ctx->rto_ms)) {
-        if (RTO != 0)
-            ctx->rto_ms = RTO;
-        else
-            ctx->rto_ms = 100;
+    if (RTO != 0) {
+      ctx->rto_ms = RTO;
+    } else if (NR_reg_get_uint4(NR_STUN_REG_PREF_CLNT_RETRANSMIT_TIMEOUT, &ctx->rto_ms)) {
+      ctx->rto_ms = 100;
     }
 
     if (NR_reg_get_double(NR_STUN_REG_PREF_CLNT_RETRANSMIT_BACKOFF, &ctx->retransmission_backoff_factor))
-        ctx->retransmission_backoff_factor = 2.0;
+      ctx->retransmission_backoff_factor = 2.0;
 
     if (NR_reg_get_uint4(NR_STUN_REG_PREF_CLNT_MAXIMUM_TRANSMITS, &ctx->maximum_transmits))
-        ctx->maximum_transmits = 7;
+      ctx->maximum_transmits = 7;
 
-    if (NR_reg_get_uint4(NR_STUN_REG_PREF_CLNT_FINAL_RETRANSMIT_BACKOFF, &ctx->final_retransmit_backoff_ms))
-        ctx->final_retransmit_backoff_ms = 16 * ctx->rto_ms;
+    if (NR_reg_get_uint4(NR_STUN_REG_PREF_CLNT_FINAL_RETRANSMIT_BACKOFF, &ctx->maximum_transmits_timeout_ms))
+      ctx->maximum_transmits_timeout_ms = 16 * ctx->rto_ms;
 
-     ctx->mapped_addr_check_mask = NR_STUN_TRANSPORT_ADDR_CHECK_WILDCARD;
-     if (NR_reg_get_char(NR_STUN_REG_PREF_ALLOW_LOOPBACK_ADDRS, &allow_loopback) ||
-         !allow_loopback) {
-       ctx->mapped_addr_check_mask |= NR_STUN_TRANSPORT_ADDR_CHECK_LOOPBACK;
-     }
+    ctx->mapped_addr_check_mask = NR_STUN_TRANSPORT_ADDR_CHECK_WILDCARD;
+    if (NR_reg_get_char(NR_STUN_REG_PREF_ALLOW_LOOPBACK_ADDRS, &allow_loopback) ||
+        !allow_loopback) {
+      ctx->mapped_addr_check_mask |= NR_STUN_TRANSPORT_ADDR_CHECK_LOOPBACK;
+    }
 
-    /* If we are doing TCP, compute the maximum timeout as if
-       we retransmitted and then set the maximum number of
-       transmits to 1 and the timeout to maximum timeout*/
     if (ctx->my_addr.protocol == IPPROTO_TCP) {
-      ctx->timeout_ms = ctx->final_retransmit_backoff_ms;
+      /* Because TCP is reliable there is only one final timeout value.
+       * We store the timeout value for TCP in here, because timeout_ms gets
+       * reset to 0 in client_reset() which gets called from client_start() */
+      ctx->maximum_transmits_timeout_ms = ctx->rto_ms *
+                        pow(ctx->retransmission_backoff_factor,
+                            ctx->maximum_transmits);
       ctx->maximum_transmits = 1;
     }
 
     *ctxp=ctx;
 
     _status=0;
   abort:
     if(_status){
@@ -388,28 +389,35 @@ static int nr_stun_client_send_request(n
 
     ctx->request_ct++;
 
     if (NR_STUN_GET_TYPE_CLASS(ctx->request->header.type) == NR_CLASS_INDICATION) {
         /* no need to set the timer because indications don't receive a
          * response */
     }
     else {
-        if (ctx->request_ct < ctx->maximum_transmits) {
-            ctx->timeout_ms *= ctx->retransmission_backoff_factor;
-            ctx->timeout_ms += ctx->rto_ms;
+        if (ctx->request_ct >= ctx->maximum_transmits) {
+          /* Reliable transport only get here once. Unreliable get here for
+           * their final timeout. */
+          ctx->timeout_ms += ctx->maximum_transmits_timeout_ms;
+        }
+        else if (ctx->timeout_ms) {
+          /* exponential backoff */
+          ctx->timeout_ms *= ctx->retransmission_backoff_factor;
         }
         else {
-            ctx->timeout_ms += ctx->final_retransmit_backoff_ms;
+          /* initial timeout unreliable transports */
+          ctx->timeout_ms = ctx->rto_ms;
         }
 
         r_log(NR_LOG_STUN,LOG_DEBUG,"STUN-CLIENT(%s): Next timer will fire in %u ms",ctx->label, ctx->timeout_ms);
 
         gettimeofday(&ctx->timer_set, 0);
 
+        assert(ctx->timeout_ms);
         NR_ASYNC_TIMER_SET(ctx->timeout_ms, nr_stun_client_timer_expired_cb, ctx, &ctx->timer_handle);
     }
 
     _status=0;
   abort:
     if (_status) {
       nr_stun_client_failed(ctx);
     }
--- a/media/mtransport/third_party/nICEr/src/stun/stun_client_ctx.h
+++ b/media/mtransport/third_party/nICEr/src/stun/stun_client_ctx.h
@@ -165,17 +165,17 @@ struct nr_stun_client_ctx_ {
   nr_stun_client_results results;
   char *nonce;
   char *realm;
   void *timer_handle;
   int request_ct;
   UINT4 rto_ms;    /* retransmission time out */
   double retransmission_backoff_factor;
   UINT4 maximum_transmits;
-  UINT4 final_retransmit_backoff_ms;
+  UINT4 maximum_transmits_timeout_ms;
   UINT4 mapped_addr_check_mask;  /* What checks to run on mapped addresses */
   int timeout_ms;
   struct timeval timer_set;
   int retry_ct;
   NR_async_cb finished_cb;
   void *cb_arg;
   nr_stun_message *request;
   nr_stun_message *response;
--- a/mobile/android/base/gfx/DisplayPortCalculator.java
+++ b/mobile/android/base/gfx/DisplayPortCalculator.java
@@ -17,19 +17,16 @@ import android.util.Log;
 
 import java.util.HashMap;
 import java.util.Map;
 
 final class DisplayPortCalculator {
     private static final String LOGTAG = "GeckoDisplayPort";
     private static final PointF ZERO_VELOCITY = new PointF(0, 0);
 
-    // Keep this in sync with the TILEDLAYERBUFFER_TILE_SIZE defined in gfx/layers/TiledLayerBuffer.h
-    private static final int TILE_SIZE = 256;
-
     private static final String PREF_DISPLAYPORT_STRATEGY = "gfx.displayport.strategy";
     private static final String PREF_DISPLAYPORT_FM_MULTIPLIER = "gfx.displayport.strategy_fm.multiplier";
     private static final String PREF_DISPLAYPORT_FM_DANGER_X = "gfx.displayport.strategy_fm.danger_x";
     private static final String PREF_DISPLAYPORT_FM_DANGER_Y = "gfx.displayport.strategy_fm.danger_y";
     private static final String PREF_DISPLAYPORT_VB_MULTIPLIER = "gfx.displayport.strategy_vb.multiplier";
     private static final String PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD = "gfx.displayport.strategy_vb.threshold";
     private static final String PREF_DISPLAYPORT_VB_REVERSE_BUFFER = "gfx.displayport.strategy_vb.reverse_buffer";
     private static final String PREF_DISPLAYPORT_VB_DANGER_X_BASE = "gfx.displayport.strategy_vb.danger_x_base";
@@ -168,30 +165,29 @@ final class DisplayPortCalculator {
         float dangerZoneX = metrics.getWidth() * dangerZoneXMultiplier;
         float dangerZoneY = metrics.getHeight() * dangerZoneYMultiplier;
         rect = RectUtils.expand(rect, dangerZoneX, dangerZoneY);
         // clamp to page bounds
         return clampToPageBounds(rect, metrics);
     }
 
     /**
-     * Expand the given margins such that when they are applied on the viewport, the resulting rect
-     * does not have any partial tiles, except when it is clipped by the page bounds. This assumes
-     * the tiles are TILE_SIZE by TILE_SIZE and start at the origin, such that there will always be
-     * a tile at (0,0)-(TILE_SIZE,TILE_SIZE)).
+     * Calculate the display port by expanding the viewport by the specified
+     * margins, then clamping to the page size.
      */
-    private static DisplayPortMetrics getTileAlignedDisplayPortMetrics(RectF margins, float zoom, ImmutableViewportMetrics metrics) {
+    private static DisplayPortMetrics getPageClampedDisplayPortMetrics(RectF margins, float zoom, ImmutableViewportMetrics metrics) {
         float left = metrics.viewportRectLeft - margins.left;
         float top = metrics.viewportRectTop - margins.top;
         float right = metrics.viewportRectRight() + margins.right;
         float bottom = metrics.viewportRectBottom() + margins.bottom;
-        left = (float) Math.max(metrics.pageRectLeft, TILE_SIZE * Math.floor(left / TILE_SIZE));
-        top = (float) Math.max(metrics.pageRectTop, TILE_SIZE * Math.floor(top / TILE_SIZE));
-        right = (float) Math.min(metrics.pageRectRight, TILE_SIZE * Math.ceil(right / TILE_SIZE));
-        bottom = (float) Math.min(metrics.pageRectBottom, TILE_SIZE * Math.ceil(bottom / TILE_SIZE));
+        left = Math.max(metrics.pageRectLeft, left);
+        top = Math.max(metrics.pageRectTop, top);
+        right = Math.min(metrics.pageRectRight, right);
+        bottom = Math.min(metrics.pageRectBottom, bottom);
+
         return new DisplayPortMetrics(left, top, right, bottom, zoom);
     }
 
     /**
      * Adjust the given margins so if they are applied on the viewport in the metrics, the resulting rect
      * does not exceed the page bounds. This code will maintain the total margin amount for a given axis;
      * it assumes that margins.left + metrics.getWidth() + margins.right is less than or equal to
      * metrics.getPageWidth(); and the same for the y axis.
@@ -306,17 +302,17 @@ final class DisplayPortCalculator {
             // is split).
             RectF margins = new RectF();
             margins.left = horizontalBuffer / 2.0f;
             margins.right = horizontalBuffer - margins.left;
             margins.top = verticalBuffer / 2.0f;
             margins.bottom = verticalBuffer - margins.top;
             margins = shiftMarginsForPageBounds(margins, metrics);
 
-            return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
+            return getPageClampedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
         }
 
         @Override
         public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
             // Increase the size of the viewport based on the danger zone multiplier (and clamp to page
             // boundaries), and intersect it with the current displayport to determine whether we're
             // close to checkerboarding.
             RectF adjustedViewport = expandByDangerZone(metrics.getViewport(), DANGER_ZONE_X_MULTIPLIER, DANGER_ZONE_Y_MULTIPLIER, metrics);
@@ -417,17 +413,17 @@ final class DisplayPortCalculator {
             float horizontalBuffer = displayPortWidth - metrics.getWidth();
             float verticalBuffer = displayPortHeight - metrics.getHeight();
 
             // split the buffer amounts into margins based on velocity, and shift it to
             // take into account the page bounds
             RectF margins = velocityBiasedMargins(horizontalBuffer, verticalBuffer, velocity);
             margins = shiftMarginsForPageBounds(margins, metrics);
 
-            return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
+            return getPageClampedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
         }
 
         @Override
         public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
             // calculate the danger zone amounts based on the prefs
             float dangerZoneX = metrics.getWidth() * (DANGER_ZONE_BASE_X_MULTIPLIER + (velocity.x * DANGER_ZONE_INCR_X_MULTIPLIER));
             float dangerZoneY = metrics.getHeight() * (DANGER_ZONE_BASE_Y_MULTIPLIER + (velocity.y * DANGER_ZONE_INCR_Y_MULTIPLIER));
             // clamp it such that when added to the viewport, they don't exceed page size.
@@ -684,17 +680,17 @@ final class DisplayPortCalculator {
         public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) {
             float width = metrics.getWidth();
             float height = metrics.getHeight();
             mPixelArea = (int)(width * height);
 
             if (velocity.length() < VELOCITY_THRESHOLD) {
                 // if we're going slow, expand the displayport to 9x viewport size
                 RectF margins = new RectF(width, height, width, height);
-                return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
+                return getPageClampedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
             }
 
             // figure out how far we expect to be
             float minDx = velocity.x * mMinFramesToDraw;
             float minDy = velocity.y * mMinFramesToDraw;
             float maxDx = velocity.x * mMaxFramesToDraw;
             float maxDy = velocity.y * mMaxFramesToDraw;
 
@@ -709,17 +705,17 @@ final class DisplayPortCalculator {
 
             // and finally generate the displayport. the min/max stuff takes care of
             // negative velocities as well as positive.
             RectF margins = new RectF(
                 -Math.min(minDx, maxDx),
                 -Math.min(minDy, maxDy),
                 Math.max(minDx, maxDx),
                 Math.max(minDy, maxDy));
-            return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
+            return getPageClampedDisplayPortMetrics(margins, metrics.zoomFactor, metrics);
         }
 
         @Override
         public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) {
             // the code below is the same as in calculate() but is awkward to refactor since it has multiple outputs.
             // refer to the comments in calculate() to understand what this is doing.
             float minDx = velocity.x * mMinFramesToDraw;
             float minDy = velocity.y * mMinFramesToDraw;
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -730,16 +730,17 @@ just addresses the organization to follo
      a restricted profile (e.g. child). Used inside the Android settings UI. -->
 <!ENTITY restriction_disallow_addons_title2 "Disable add-on installation">
 <!ENTITY restriction_disallow_private_browsing_title2 "Disable Private Browsing">
 <!ENTITY restriction_disallow_location_services_title2 "Disable Location Services">
 <!ENTITY restriction_disallow_clear_history_title2 "Disable \'Clear browsing history\'">
 <!ENTITY restriction_disallow_master_password_title2 "Disable master password">
 <!ENTITY restriction_disallow_guest_browsing_title2 "Disable Guest Browsing">
 <!ENTITY restriction_disallow_advanced_settings_title "Disable Advanced Settings">
+<!ENTITY restriction_disallow_camera_microphone_title "Block camera and microphone">
 
 <!-- Default Bookmarks titles-->
 <!-- LOCALIZATION NOTE (bookmarks_about_browser): link title for about:fennec -->
 <!ENTITY bookmarks_about_browser "Firefox: About your browser">
 <!-- LOCALIZATION NOTE (bookmarks_addons): link title for https://addons.mozilla.org/en-US/mobile -->
 <!ENTITY bookmarks_addons "Firefox: Customize with add-ons">
 <!-- LOCALIZATION NOTE (bookmarks_support): link title for https://support.mozilla.org/ -->
 <!ENTITY bookmarks_support "Firefox: Support">
--- a/mobile/android/base/restrictions/RestrictedProfileConfiguration.java
+++ b/mobile/android/base/restrictions/RestrictedProfileConfiguration.java
@@ -23,17 +23,18 @@ public class RestrictedProfileConfigurat
     static List<Restriction> DEFAULT_RESTRICTIONS = Arrays.asList(
             Restriction.DISALLOW_INSTALL_EXTENSION,
             Restriction.DISALLOW_PRIVATE_BROWSING,
             Restriction.DISALLOW_LOCATION_SERVICE,
             Restriction.DISALLOW_CLEAR_HISTORY,
             Restriction.DISALLOW_MASTER_PASSWORD,
             Restriction.DISALLOW_GUEST_BROWSING,
             Restriction.DISALLOW_DEFAULT_THEME,
-            Restriction.DISALLOW_ADVANCED_SETTINGS
+            Restriction.DISALLOW_ADVANCED_SETTINGS,
+            Restriction.DISALLOW_CAMERA_MICROPHONE
     );
 
     private Context context;
     private Bundle cachedRestrictions;
     private boolean isCacheInvalid = true;
 
     public RestrictedProfileConfiguration(Context context) {
         this.context = context.getApplicationContext();
--- a/mobile/android/base/restrictions/Restriction.java
+++ b/mobile/android/base/restrictions/Restriction.java
@@ -47,17 +47,19 @@ public enum Restriction {
     DISALLOW_CLEAR_HISTORY(14, "no_clear_history", R.string.restriction_disallow_clear_history_title),
 
     DISALLOW_MASTER_PASSWORD(15, "no_master_password", R.string.restriction_disallow_master_password_title),
 
     DISALLOW_GUEST_BROWSING(16, "no_guest_browsing",  R.string.restriction_disallow_guest_browsing_title),
 
     DISALLOW_DEFAULT_THEME(17, "no_default_theme", 0),
 
-    DISALLOW_ADVANCED_SETTINGS(18, "no_advanced_settings", R.string.restriction_disallow_advanced_settings_title);
+    DISALLOW_ADVANCED_SETTINGS(18, "no_advanced_settings", R.string.restriction_disallow_advanced_settings_title),
+
+    DISALLOW_CAMERA_MICROPHONE(19, "no_camera_microphone", R.string.restriction_disallow_camera_microphone_title);
 
     public final int id;
     public final String name;
 
     @StringRes
     public final int title;
 
     Restriction(final int id, final String name, @StringRes int title) {
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -574,16 +574,17 @@
   <!-- Restrictions -->
   <string name="restriction_disallow_addons_title">&restriction_disallow_addons_title2;</string>
   <string name="restriction_disallow_private_browsing_title">&restriction_disallow_private_browsing_title2;</string>
   <string name="restriction_disallow_location_services_title">&restriction_disallow_location_services_title2;</string>
   <string name="restriction_disallow_clear_history_title">&restriction_disallow_clear_history_title2;</string>
   <string name="restriction_disallow_master_password_title">&restriction_disallow_master_password_title2;</string>
   <string name="restriction_disallow_guest_browsing_title">&restriction_disallow_guest_browsing_title2;</string>
   <string name="restriction_disallow_advanced_settings_title">&restriction_disallow_advanced_settings_title;</string>
+  <string name="restriction_disallow_camera_microphone_title">&restriction_disallow_camera_microphone_title;</string>
 
   <!-- Miscellaneous -->
   <string name="ellipsis">&ellipsis;</string>
 
   <string name="colon">&colon;</string>
 
   <string name="percent">&percent;</string>
 
--- a/mobile/android/chrome/content/WebrtcUI.js
+++ b/mobile/android/chrome/content/WebrtcUI.js
@@ -1,16 +1,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/. */
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["WebrtcUI"];
 
 XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "ParentalControls", "@mozilla.org/parental-controls-service;1", "nsIParentalControlsService");
 
 var WebrtcUI = {
   _notificationId: null,
 
   // Add-ons can override stock permission behavior by doing:
   //
   //   var stockObserve = WebrtcUI.observe;
   //
@@ -103,16 +104,22 @@ var WebrtcUI = {
 
   handleGumRequest: function handleGumRequest(aSubject, aTopic, aData) {
     let constraints = aSubject.getConstraints();
     let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID);
 
     contentWindow.navigator.mozGetUserMediaDevices(
       constraints,
       function (devices) {
+        if (!ParentalControls.isAllowed(ParentalControls.CAMERA_MICROPHONE)) {
+          Services.obs.notifyObservers(null, "getUserMedia:response:deny", aSubject.callID);
+          WebrtcUI.showBlockMessage(devices);
+          return;
+        }
+
         WebrtcUI.prompt(contentWindow, aSubject.callID, constraints.audio,
                         constraints.video, devices);
       },
       function (error) {
         Cu.reportError(error);
       },
       aSubject.innerWindowID);
   },
@@ -182,16 +189,41 @@ var WebrtcUI = {
           label: Strings.browser.GetStringFromName("getUserMedia." + aType + ".prompt"),
           values: list
         });
 
       }
     }
   },
 
+  showBlockMessage: function(aDevices) {
+    let microphone = false;
+    let camera = false;
+
+    for (let device of aDevices) {
+      device = device.QueryInterface(Ci.nsIMediaDevice);
+      if (device.type == "audio") {
+        microphone = true;
+      } else if (device.type == "video") {
+        camera = true;
+      }
+    }
+
+    let message;
+    if (microphone && !camera) {
+      message = Strings.browser.GetStringFromName("getUserMedia.blockedMicrophoneAccess");
+    } else if (camera && !microphone) {
+      message = Strings.browser.GetStringFromName("getUserMedia.blockedCameraAccess");
+    } else {
+      message = Strings.browser.GetStringFromName("getUserMedia.blockedCameraAndMicrophoneAccess");
+    }
+
+    NativeWindow.doorhanger.show(message, "webrtc-blocked", [], BrowserApp.selectedTab.id, {});
+  },
+
   prompt: function prompt(aContentWindow, aCallID, aAudioRequested,
                           aVideoRequested, aDevices) {
     let audioDevices = [];
     let videoDevices = [];
     for (let device of aDevices) {
       device = device.QueryInterface(Ci.nsIMediaDevice);
       switch (device.type) {
       case "audio":
--- a/mobile/android/locales/en-US/chrome/browser.properties
+++ b/mobile/android/locales/en-US/chrome/browser.properties
@@ -392,16 +392,19 @@ getUserMedia.videoSource.none = No Video
 getUserMedia.videoSource.tabShare = Choose a tab to stream
 getUserMedia.videoSource.prompt = Video source
 getUserMedia.audioDevice.default = Microphone %S
 getUserMedia.audioDevice.none = No Audio
 getUserMedia.audioDevice.prompt = Microphone to use
 getUserMedia.sharingCamera.message2 = Camera is on
 getUserMedia.sharingMicrophone.message2 = Microphone is on
 getUserMedia.sharingCameraAndMicrophone.message2 = Camera and microphone are on
+getUserMedia.blockedCameraAccess = Camera has been blocked.
+getUserMedia.blockedMicrophoneAccess = Microphone has been blocked.
+getUserMedia.blockedCameraAndMicrophoneAccess = Camera and microphone have been blocked.
 
 # LOCALIZATION NOTE (readerMode.toolbarTip):
 # Tip shown to users the first time we hide the reader mode toolbar.
 readerMode.toolbarTip=Tap the screen to show reader options
 
 #Open in App
 openInApp.pageAction = Open in App
 openInApp.ok = OK
--- a/security/manager/ssl/WeakCryptoOverride.cpp
+++ b/security/manager/ssl/WeakCryptoOverride.cpp
@@ -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/. */
 
 #include "WeakCryptoOverride.h"
 
 #include "MainThreadUtils.h"
 #include "SharedSSLState.h"
+#include "nss.h"
 
 using namespace mozilla;
 using namespace mozilla::psm;
 
 NS_IMPL_ISUPPORTS(WeakCryptoOverride,
                   nsIWeakCryptoOverride)
 
 WeakCryptoOverride::WeakCryptoOverride()
@@ -53,10 +54,14 @@ WeakCryptoOverride::RemoveWeakCryptoOver
   SharedSSLState* sharedState = aPrivate ? PrivateSSLState()
                                          : PublicSSLState();
   if (!sharedState) {
     return NS_ERROR_NOT_AVAILABLE;
   }
   const nsPromiseFlatCString& host = PromiseFlatCString(aHostName);
   sharedState->IOLayerHelpers().removeInsecureFallbackSite(host, aPort);
 
+  // Some servers will fail with SSL_ERROR_ILLEGAL_PARAMETER_ALERT
+  // unless the session cache is cleared.
+  SSL_ClearSessionCache();
+
   return NS_OK;
 }
--- a/testing/marionette/client/marionette/tests/unit/test_emulator.py
+++ b/testing/marionette/client/marionette/tests/unit/test_emulator.py
@@ -7,40 +7,37 @@ from unittest import skip
 from marionette.marionette_test import MarionetteTestCase, skip_if_desktop, skip_unless_protocol
 from marionette_driver.errors import MarionetteException, JavascriptException
 
 
 class TestEmulatorContent(MarionetteTestCase):
     @skip_if_desktop
     def test_emulator_cmd(self):
         self.marionette.set_script_timeout(10000)
-        expected = ["<build>",
-                    "OK"]
-        result = self.marionette.execute_async_script("""
-        runEmulatorCmd("avd name", marionetteScriptFinished)
-        """);
-        self.assertEqual(result, expected)
+        expected = ["<build>", "OK"]
+        res = self.marionette.execute_async_script(
+            "runEmulatorCmd('avd name', marionetteScriptFinished)");
+        self.assertEqual(res, expected)
 
     @skip_if_desktop
     def test_emulator_shell(self):
         self.marionette.set_script_timeout(10000)
         expected = ["Hello World!"]
-        result = self.marionette.execute_async_script("""
-        runEmulatorShell(["echo", "Hello World!"], marionetteScriptFinished)
-        """);
-        self.assertEqual(result, expected)
+        res = self.marionette.execute_async_script(
+            "runEmulatorShell(['echo', 'Hello World!'], marionetteScriptFinished)")
+        self.assertEqual(res, expected)
 
     @skip_if_desktop
     def test_emulator_order(self):
         self.marionette.set_script_timeout(10000)
-        self.assertRaises(MarionetteException,
-                          self.marionette.execute_async_script,
-        """runEmulatorCmd("gsm status", function(result) {});
-           marionetteScriptFinished(true);
-        """);
+        with self.assertRaises(MarionetteException):
+            self.marionette.execute_async_script("""
+               runEmulatorCmd("gsm status", function(res) {});
+               marionetteScriptFinished(true);
+               """)
 
 
 class TestEmulatorChrome(TestEmulatorContent):
     def setUp(self):
         super(TestEmulatorChrome, self).setUp()
         self.marionette.set_context("chrome")
 
 
@@ -127,11 +124,29 @@ class TestEmulatorCallbacks(MarionetteTe
             self.assertEqual("shell response", res)
 
     @skip_unless_protocol(lambda level: level >= 3)
     def test_emulator_result_error_chrome(self):
         with self.marionette.using_context("chrome"):
             with self.assertRaisesRegexp(JavascriptException, "TypeError"):
                 self.marionette.execute_async_script("runEmulatorCmd()")
 
+    def test_multiple_callbacks(self):
+        res = self.marionette.execute_async_script("""
+            runEmulatorCmd("what");
+            runEmulatorCmd("ho");
+            marionetteScriptFinished("Frobisher");
+            """)
+        self.assertEqual("Frobisher", res)
+
+    # This should work, but requires work on emulator callbacks:
+    """
+    def test_multiple_nested_callbacks(self):
+        res = self.marionette.execute_async_script('''
+            runEmulatorCmd("what", function(res) {
+              runEmulatorCmd("ho", marionetteScriptFinished);
+            });''')
+        self.assertEqual("cmd response", res)
+    """
+
 
 def escape(word):
     return "'%s'" % word
--- a/testing/marionette/driver/marionette_driver/marionette.py
+++ b/testing/marionette/driver/marionette_driver/marionette.py
@@ -666,16 +666,37 @@ class Marionette(object):
         finally:
             s.close()
 
     def wait_for_port(self, timeout=60):
         return transport.wait_for_port(self.host, self.port, timeout=timeout)
 
     @do_crash_check
     def _send_message(self, name, params=None, key=None):
+        """Send a blocking message to the server.
+
+        Marionette provides an asynchronous, non-blocking interface and
+        this attempts to paper over this by providing a synchronous API
+        to the user.
+
+        In particular, the Python client can be instructed to carry out
+        a sequence of instructions on the connected emulator.  For this
+        reason, if ``execute_script``, ``execute_js_script``, or
+        ``execute_async_script`` is called, it will loop until all
+        commands requested from the server have been exhausted, and we
+        receive our expected response.
+
+        :param name: Requested command key.
+        :param params: Optional dictionary of key/value arguments.
+        :param key: Optional key to extract from response.
+
+        :returns: Full response from the server, or if `key` is given,
+            the value of said key in the response.
+        """
+
         if not self.session_id and name != "newSession":
             raise errors.MarionetteException("Please start a session")
 
         try:
             if self.protocol < 3:
                 data = {"name": name}
                 if params:
                     data["parameters"] = params
@@ -687,23 +708,26 @@ class Marionette(object):
 
         except IOError:
             if self.instance and not hasattr(self.instance, 'detached'):
                 # If we've launched the binary we've connected to, wait
                 # for it to shut down.
                 returncode = self.instance.runner.wait(timeout=self.DEFAULT_STARTUP_TIMEOUT)
                 raise IOError("process died with returncode %s" % returncode)
             raise
+
         except socket.timeout:
             self.session = None
             self.window = None
             self.client.close()
             raise errors.TimeoutException("Connection timed out")
 
-        if isinstance(msg, transport.Command):
+        # support execution of commands on the client,
+        # loop until we receive our expected response
+        while isinstance(msg, transport.Command):
             if msg.name == "runEmulatorCmd":
                 self.emulator_callback_id = msg.params.get("id")
                 msg = self._emulator_cmd(msg.params["emulator_cmd"])
             elif msg.name == "runEmulatorShell":
                 self.emulator_callback_id = msg.params.get("id")
                 msg = self._emulator_shell(msg.params["emulator_shell"])
             else:
                 raise IOError("Unknown command: %s" % msg)
--- a/testing/marionette/proxy.js
+++ b/testing/marionette/proxy.js
@@ -109,17 +109,17 @@ ContentSender.prototype.send = function(
 
   this.curId = uuidgen.generateUUID().toString();
 
   let proxy = new Promise((resolve, reject) => {
     let removeListeners = (n, fn) => {
       let rmFn = msg => {
         if (this.curId !== msg.json.command_id) {
           logger.warn("Skipping out-of-sync response from listener: " +
-              `Expected response to \`${name}' with ID ${this.curId}, ` +
+              `Expected response to ${name} with ID ${this.curId}, ` +
               "but got: " + msg.name + msg.json.toSource());
           return;
         }
 
         this.removeListeners();
         modal.removeHandler(handleDialog);
 
         fn(msg);
--- a/testing/web-platform/mozilla/tests/service-workers/service-worker/serviceworkerobject-scripturl.https.html
+++ b/testing/web-platform/mozilla/tests/service-workers/service-worker/serviceworkerobject-scripturl.https.html
@@ -16,12 +16,11 @@ function url_test(name, url) {
                         'Returned ServiceWorker object should have scriptURL');
           service_worker_unregister_and_done(t, scope);
         })
       .catch(unreached_rejection(t));
   }, 'Verify the scriptURL property: ' + name);
 }
 
 url_test('relative', 'resources/empty-worker.js');
-url_test('with-fragment', 'resources/empty-worker.js#ref');
 url_test('absolute', (new URL('./resources/empty-worker.js', window.location)).href);
 
 </script>
--- a/toolkit/components/parentalcontrols/nsIParentalControlsService.idl
+++ b/toolkit/components/parentalcontrols/nsIParentalControlsService.idl
@@ -6,17 +6,17 @@
 
 #include "nsISupports.idl"
 
 interface nsIURI;
 interface nsIFile;
 interface nsIInterfaceRequestor;
 interface nsIArray;
 
-[scriptable, uuid(8c4962aa-e0e0-482e-b1db-7846cb78b3d6)]
+[scriptable, uuid(ca8eb4f8-89bc-4479-91c9-1f381e045ed7)]
 interface nsIParentalControlsService : nsISupports
 {
   /**
    * Action types that can be blocked for users.
    */
   const short DOWNLOAD = 1; // Downloading files
   const short INSTALL_EXTENSION = 2; // Installing extensions
   const short INSTALL_APP = 3; // Installing webapps
@@ -30,16 +30,17 @@ interface nsIParentalControlsService : n
   const short IMPORT_SETTINGS = 11; // Importing settings from other apps
   const short PRIVATE_BROWSING = 12; // Disallow usage of private browsing
   const short LOCATION_SERVICE = 13; // Sharing of location data to location service
   const short CLEAR_HISTORY = 14; // Clear browsing history
   const short MASTER_PASSWORD = 15; // Setting master password for logins
   const short GUEST_BROWSING = 16; // Disallow usage of guest browsing
   const short DEFAULT_THEME = 17; // Use default theme or a special parental controls theme
   const short ADVANCED_SETTINGS = 18; // Advanced settings
+  const short CAMERA_MICROPHONE = 19; // Camera and microphone (WebRTC)
 
   /**
    * @returns true if the current user account has parental controls
    * restrictions enabled.
    */
   readonly attribute boolean parentalControlsEnabled;
 
   /**
--- a/toolkit/components/passwordmgr/LoginManagerContent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -17,16 +17,20 @@ Cu.import("resource://gre/modules/Promis
 
 XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LoginRecipesContent",
                                   "resource://gre/modules/LoginRecipes.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
                                   "resource://gre/modules/LoginHelper.jsm");
 
+XPCOMUtils.defineLazyServiceGetter(this, "gContentSecurityManager",
+                                   "@mozilla.org/contentsecuritymanager;1",
+                                   "nsIContentSecurityManager");
+
 XPCOMUtils.defineLazyGetter(this, "log", () => {
   let logger = LoginHelper.createLogger("LoginManagerContent");
   return logger.log.bind(logger);
 });
 
 // These mirror signon.* prefs.
 var gEnabled, gAutofillForms, gStoreWhenAutocompleteOff;
 
@@ -1125,17 +1129,19 @@ var LoginManagerContent = {
    *   both places. Look at
    *   https://bugzilla.mozilla.org/show_bug.cgi?id=899099 for more info.
    */
   checkIfURIisSecure : function(uri) {
     let isSafe = false;
     let netutil = Cc["@mozilla.org/network/util;1"].getService(Ci.nsINetUtil);
     let ph = Ci.nsIProtocolHandler;
 
-    if (netutil.URIChainHasFlags(uri, ph.URI_IS_LOCAL_RESOURCE) ||
+    // Is the connection to localhost? Consider localhost safe for passwords.
+    if (gContentSecurityManager.isURIPotentiallyTrustworthy(uri) ||
+        netutil.URIChainHasFlags(uri, ph.URI_IS_LOCAL_RESOURCE) ||
         netutil.URIChainHasFlags(uri, ph.URI_DOES_NOT_RETURN_DATA) ||
         netutil.URIChainHasFlags(uri, ph.URI_INHERITS_SECURITY_CONTEXT) ||
         netutil.URIChainHasFlags(uri, ph.URI_SAFE_TO_LOAD_IN_SECURE_CONTEXT)) {
 
       isSafe = true;
     }
 
     return isSafe;
--- a/toolkit/components/telemetry/TelemetryController.jsm
+++ b/toolkit/components/telemetry/TelemetryController.jsm
@@ -759,17 +759,16 @@ var Impl = {
 
         // Load the ClientID.
         this._clientID = yield ClientID.getClientID();
 
         // Purge the pings archive by removing outdated pings. We don't wait for this
         // task to complete, but TelemetryStorage blocks on it during shutdown.
         TelemetryStorage.runCleanPingArchiveTask();
 
-        Telemetry.asyncFetchTelemetryData(function () {});
         this._delayedInitTaskDeferred.resolve();
       } catch (e) {
         this._delayedInitTaskDeferred.reject(e);
       } finally {
         this._delayedInitTask = null;
       }
     }.bind(this), this._testMode ? TELEMETRY_TEST_DELAY : TELEMETRY_DELAY);
 
deleted file mode 100644
--- a/toolkit/crashreporter/google-breakpad/src/common/mac/Makefile.in
+++ /dev/null
@@ -1,1 +0,0 @@
-%/dump_syms.mm: ;
deleted file mode 100644
--- a/toolkit/crashreporter/google-breakpad/src/tools/mac/dump_syms/Makefile.in
+++ /dev/null
@@ -1,1 +0,0 @@
-%/dump_syms_tool.mm: ;
--- a/toolkit/modules/ClientID.jsm
+++ b/toolkit/modules/ClientID.jsm
@@ -169,16 +169,19 @@ var ClientIDImpl = {
   getCachedClientID: function() {
     if (this._clientID) {
       // Already loaded the client id from disk.
       return this._clientID;
     }
 
     // Not yet loaded, return the cached client id if we have one.
     let id = Preferences.get(PREF_CACHED_CLIENTID, null);
+    if (id === null) {
+      return null;
+    }
     if (!isValidClientID(id)) {
       this._log.error("getCachedClientID - invalid client id in preferences, resetting", id);
       Preferences.reset(PREF_CACHED_CLIENTID);
       return null;
     }
     return id;
   },
 
--- a/xpcom/idl-parser/xpidl/header.py
+++ b/xpcom/idl-parser/xpidl/header.py
@@ -106,16 +106,29 @@ def paramlistAsNative(m, empty='void'):
     if not m.notxpcom and m.realtype.name != 'void':
         l.append(paramAsNative(xpidl.Param(paramtype='out',
                                            type=None,
                                            name='_retval',
                                            attlist=[],
                                            location=None,
                                            realtype=m.realtype)))
 
+    # Set any optional out params to default to nullptr. Skip if we just added
+    # extra non-optional args to l.
+    if len(l) == len(m.params):
+        paramIter = len(m.params) - 1
+        while (paramIter >= 0 and m.params[paramIter].optional and
+                m.params[paramIter].paramtype == "out"):
+            t = m.params[paramIter].type
+            # Strings can't be optional, so this shouldn't happen, but let's make sure:
+            if t == "AString" or t == "ACString" or t == "DOMString" or t == "AUTF8String":
+                break
+            l[paramIter] += " = nullptr"
+            paramIter -= 1
+
     if len(l) == 0:
         return empty
 
     return ", ".join(l)
 
 
 def paramAsNative(p):
     return "%s%s" % (p.nativeType(),