Merge autoland to central, a=merge
authorWes Kocher <wkocher@mozilla.com>
Mon, 31 Jul 2017 16:43:19 -0700
changeset 420765 8b19670d12fde57d3aee50a5a7d1c734d9b709d5
parent 420675 87824406b9feb420a3150720707b424d7cee5915 (current diff)
parent 420764 7423fc05e05b5d8c2448da9a0517a1c366199c32 (diff)
child 420797 44121dbcac6a9d3ff18ed087a09b3205e5a04db1
push id7566
push usermtabara@mozilla.com
push dateWed, 02 Aug 2017 08:25:16 +0000
treeherdermozilla-beta@86913f512c3c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone56.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge autoland to central, a=merge MozReview-Commit-ID: 1vnfzrwONu9
new file mode 100644
--- /dev/null
+++ b/browser/base/content/browser-pageActions.js
@@ -0,0 +1,685 @@
+/* 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/. */
+
+var BrowserPageActions = {
+  /**
+   * The main page action button in the urlbar (DOM node)
+   */
+  get mainButtonNode() {
+    delete this.mainButtonNode;
+    return this.mainButtonNode = document.getElementById("pageActionButton");
+  },
+
+  /**
+   * The main page action panel DOM node (DOM node)
+   */
+  get panelNode() {
+    delete this.panelNode;
+    return this.panelNode = document.getElementById("pageActionPanel");
+  },
+
+  /**
+   * The photonmultiview node in the main page action panel (DOM node)
+   */
+  get multiViewNode() {
+    delete this.multiViewNode;
+    return this.multiViewNode = document.getElementById("pageActionPanelMultiView");
+  },
+
+  /**
+   * The main panelview node in the main page action panel (DOM node)
+   */
+  get mainViewNode() {
+    delete this.mainViewNode;
+    return this.mainViewNode = document.getElementById("pageActionPanelMainView");
+  },
+
+  /**
+   * The vbox body node in the main panelview node (DOM node)
+   */
+  get mainViewBodyNode() {
+    delete this.mainViewBodyNode;
+    return this.mainViewBodyNode = this.mainViewNode.querySelector(".panel-subview-body");
+  },
+
+  /**
+   * Inits.  Call to init.
+   */
+  init() {
+    for (let action of PageActions.actions) {
+      this.placeAction(action, PageActions.insertBeforeActionIDInUrlbar(action));
+    }
+  },
+
+  /**
+   * Adds or removes as necessary DOM nodes for the given action.
+   *
+   * @param  action (PageActions.Action, required)
+   *         The action to place.
+   * @param  panelInsertBeforeID (string, required)
+   *         The ID of the action in the panel before which the given action
+   *         action should be inserted.
+   * @param  urlbarInsertBeforeID (string, required)
+   *         If the action is shown in the urlbar, then this is ID of the action
+   *         in the urlbar before which the given action should be inserted.
+   */
+  placeAction(action, panelInsertBeforeID, urlbarInsertBeforeID) {
+    if (action.__isSeparator) {
+      this._appendPanelSeparator(action);
+      return;
+    }
+    this.placeActionInPanel(action, panelInsertBeforeID);
+    this.placeActionInUrlbar(action, urlbarInsertBeforeID);
+  },
+
+  /**
+   * Adds or removes as necessary DOM nodes for the action in the panel.
+   *
+   * @param  action (PageActions.Action, required)
+   *         The action to place.
+   * @param  insertBeforeID (string, required)
+   *         The ID of the action in the panel before which the given action
+   *         action should be inserted.
+   */
+  placeActionInPanel(action, insertBeforeID) {
+    let id = this._panelButtonNodeIDForActionID(action.id);
+    let node = document.getElementById(id);
+    if (!node) {
+      let panelViewNode;
+      [node, panelViewNode] = this._makePanelButtonNodeForAction(action);
+      node.id = id;
+      let insertBeforeNode = null;
+      if (insertBeforeID) {
+        let insertBeforeNodeID =
+          this._panelButtonNodeIDForActionID(insertBeforeID);
+        insertBeforeNode = document.getElementById(insertBeforeNodeID);
+      }
+      this.mainViewBodyNode.insertBefore(node, insertBeforeNode);
+      action.onPlacedInPanel(node);
+      if (panelViewNode) {
+        action.subview.onPlaced(panelViewNode);
+      }
+    }
+    return node;
+  },
+
+  _makePanelButtonNodeForAction(action) {
+    let buttonNode = document.createElement("toolbarbutton");
+    buttonNode.classList.add(
+      "subviewbutton",
+      "subviewbutton-iconic",
+      "pageAction-panel-button"
+    );
+    buttonNode.setAttribute("label", action.title);
+    if (action.iconURL) {
+      buttonNode.style.listStyleImage = `url('${action.iconURL}')`;
+    }
+    if (action.nodeAttributes) {
+      for (let name in action.nodeAttributes) {
+        buttonNode.setAttribute(name, action.nodeAttributes[name]);
+      }
+    }
+    let panelViewNode = null;
+    if (action.subview) {
+      buttonNode.classList.add("subviewbutton-nav");
+      panelViewNode = this._makePanelViewNodeForAction(action, false);
+      this.multiViewNode._panelViews = null;
+      this.multiViewNode.appendChild(panelViewNode);
+    }
+    buttonNode.addEventListener("command", event => {
+      if (panelViewNode) {
+        action.subview.onShowing(panelViewNode);
+        this.multiViewNode.showSubView(panelViewNode, buttonNode);
+        return;
+      }
+      if (action.wantsIframe) {
+        this._toggleTempPanelForAction(action);
+        return;
+      }
+      this.panelNode.hidePopup();
+      action.onCommand(event, buttonNode);
+    });
+    return [buttonNode, panelViewNode];
+  },
+
+  _makePanelViewNodeForAction(action, forUrlbar) {
+    let panelViewNode = document.createElement("panelview");
+    panelViewNode.id = this._panelViewNodeIDForActionID(action.id, forUrlbar);
+    panelViewNode.classList.add("PanelUI-subView");
+    let bodyNode = document.createElement("vbox");
+    bodyNode.id = panelViewNode.id + "-body";
+    bodyNode.classList.add("panel-subview-body");
+    panelViewNode.appendChild(bodyNode);
+    for (let button of action.subview.buttons) {
+      let buttonNode = document.createElement("toolbarbutton");
+      buttonNode.id =
+        this._panelViewButtonNodeIDForActionID(action.id, button.id, forUrlbar);
+      buttonNode.classList.add("subviewbutton", "subviewbutton-iconic");
+      buttonNode.setAttribute("label", button.title);
+      if (button.shortcut) {
+        buttonNode.setAttribute("shortcut", button.shortcut);
+      }
+      if (button.disabled) {
+        buttonNode.setAttribute("disabled", "true");
+      }
+      buttonNode.addEventListener("command", event => {
+        button.onCommand(event, buttonNode);
+      });
+      bodyNode.appendChild(buttonNode);
+    }
+    return panelViewNode;
+  },
+
+  _toggleTempPanelForAction(action) {
+    let panelNodeID = this._tempPanelID;
+    let panelNode = document.getElementById(panelNodeID);
+    if (panelNode) {
+      panelNode.hidePopup();
+      return;
+    }
+
+    panelNode = document.createElement("panel");
+    panelNode.id = panelNodeID;
+    panelNode.classList.add("cui-widget-panel");
+    panelNode.setAttribute("role", "group");
+    panelNode.setAttribute("type", "arrow");
+    panelNode.setAttribute("flip", "slide");
+    panelNode.setAttribute("noautofocus", "true");
+    panelNode.setAttribute("tabspecific", "true");
+
+    let panelViewNode = null;
+    let iframeNode = null;
+
+    if (action.subview) {
+      let multiViewNode = document.createElement("photonpanelmultiview");
+      panelViewNode = this._makePanelViewNodeForAction(action, true);
+      multiViewNode.appendChild(panelViewNode);
+      panelNode.appendChild(multiViewNode);
+    } else if (action.wantsIframe) {
+      iframeNode = document.createElement("iframe");
+      iframeNode.setAttribute("type", "content");
+      panelNode.appendChild(iframeNode);
+    }
+
+    let popupSet = document.getElementById("mainPopupSet");
+    popupSet.appendChild(panelNode);
+    panelNode.addEventListener("popuphidden", () => {
+      panelNode.remove();
+    }, { once: true });
+
+    if (panelViewNode) {
+      action.subview.onPlaced(panelViewNode);
+      action.subview.onShowing(panelViewNode);
+    }
+
+    this.panelNode.hidePopup();
+
+    let urlbarNodeID = this._urlbarButtonNodeIDForActionID(action.id);
+    let anchorNode =
+      document.getElementById(urlbarNodeID) || this.mainButtonNode;
+    panelNode.openPopup(anchorNode, "bottomcenter topright");
+
+    if (iframeNode) {
+      action.onIframeShown(iframeNode, panelNode);
+    }
+  },
+
+  get _tempPanelID() {
+    return "pageActionTempPanel";
+  },
+
+  /**
+   * Adds or removes as necessary a DOM node for the given action in the urlbar.
+   *
+   * @param  action (PageActions.Action, required)
+   *         The action to place.
+   * @param  insertBeforeID (string, required)
+   *         If the action is shown in the urlbar, then this is ID of the action
+   *         in the urlbar before which the given action should be inserted.
+   */
+  placeActionInUrlbar(action, insertBeforeID) {
+    let id = this._urlbarButtonNodeIDForActionID(action.id);
+    let node = document.getElementById(id);
+
+    if (!action.shownInUrlbar) {
+      if (node) {
+        if (action.__urlbarNodeInMarkup) {
+          node.hidden = true;
+        } else {
+          node.remove();
+        }
+      }
+      return null;
+    }
+
+    let newlyPlaced = false;
+    if (action.__urlbarNodeInMarkup) {
+      newlyPlaced = node && node.hidden;
+      node.hidden = false;
+    } else if (!node) {
+      newlyPlaced = true;
+      node = this._makeUrlbarButtonNode(action);
+      node.id = id;
+    }
+
+    if (newlyPlaced) {
+      let parentNode = this.mainButtonNode.parentNode;
+      let insertBeforeNode = null;
+      if (insertBeforeID) {
+        let insertBeforeNodeID =
+          this._urlbarButtonNodeIDForActionID(insertBeforeID);
+        insertBeforeNode = document.getElementById(insertBeforeNodeID);
+      }
+      parentNode.insertBefore(node, insertBeforeNode);
+      action.onPlacedInUrlbar(node);
+
+      // urlbar buttons should always have tooltips, so if the node doesn't have
+      // one, then as a last resort use the label of the corresponding panel
+      // button.  Why not set tooltiptext to action.title when the node is
+      // created?  Because the consumer may set a title dynamically.
+      if (!node.hasAttribute("tooltiptext")) {
+        let panelNodeID = this._panelButtonNodeIDForActionID(action.id);
+        let panelNode = document.getElementById(panelNodeID);
+        if (panelNode) {
+          node.setAttribute("tooltiptext", panelNode.getAttribute("label"));
+        }
+      }
+    }
+
+    return node;
+  },
+
+  _makeUrlbarButtonNode(action) {
+    let buttonNode = document.createElement("image");
+    buttonNode.classList.add("urlbar-icon");
+    if (action.tooltip) {
+      buttonNode.setAttribute("tooltiptext", action.tooltip);
+    }
+    if (action.iconURL) {
+      buttonNode.style.listStyleImage = `url('${action.iconURL}')`;
+    }
+    if (action.nodeAttributes) {
+      for (let name in action.nodeAttributes) {
+        buttonNode.setAttribute(name, action.nodeAttributes[name]);
+      }
+    }
+    buttonNode.addEventListener("click", event => {
+      if (event.button != 0) {
+        return;
+      }
+      if (action.subview || action.wantsIframe) {
+        this._toggleTempPanelForAction(action);
+        return;
+      }
+      action.onCommand(event, buttonNode);
+    });
+    return buttonNode;
+  },
+
+  _appendPanelSeparator(action) {
+    let node = document.createElement("toolbarseparator");
+    node.id = this._panelButtonNodeIDForActionID(action.id);
+    this.mainViewBodyNode.appendChild(node);
+  },
+
+  /**
+   * Removes all the DOM nodes of the given action.
+   *
+   * @param  action (PageActions.Action, required)
+   *         The action to remove.
+   */
+  removeAction(action) {
+    this._removeActionFromPanel(action);
+    this._removeActionFromUrlbar(action);
+  },
+
+  _removeActionFromPanel(action) {
+    let id = this._panelButtonNodeIDForActionID(action.id);
+    let node = document.getElementById(id);
+    if (node) {
+      node.remove();
+    }
+    if (action.subview) {
+      let panelViewNodeID = this._panelViewNodeIDForActionID(action.id, false);
+      let panelViewNode = document.getElementById(panelViewNodeID);
+      if (panelViewNode) {
+        panelViewNode.remove();
+      }
+    }
+    // If there are now no more non-built-in actions, remove the separator
+    // between the built-ins and non-built-ins.
+    if (!PageActions.nonBuiltInActions.length) {
+      let separator = document.getElementById(
+        this._panelButtonNodeIDForActionID(
+          PageActions.ACTION_ID_BUILT_IN_SEPARATOR
+        )
+      );
+      if (separator) {
+        separator.remove();
+      }
+    }
+  },
+
+  _removeActionFromUrlbar(action) {
+    let id = this._urlbarButtonNodeIDForActionID(action.id);
+    let node = document.getElementById(id);
+    if (node) {
+      node.remove();
+    }
+  },
+
+  /**
+   * Updates the DOM nodes of an action to reflect its changed iconURL.
+   *
+   * @param  action (PageActions.Action, required)
+   *         The action to update.
+   */
+  updateActionIconURL(action) {
+    let url = action.iconURL ? `url('${action.iconURL}')` : null;
+    let nodeIDs = [
+      this._panelButtonNodeIDForActionID(action.id),
+      this._urlbarButtonNodeIDForActionID(action.id),
+    ];
+    for (let nodeID of nodeIDs) {
+      let node = document.getElementById(nodeID);
+      if (node) {
+        if (url) {
+          node.style.listStyleImage = url;
+        } else {
+          node.style.removeProperty("list-style-image");
+        }
+      }
+    }
+  },
+
+  /**
+   * Updates the DOM nodes of an action to reflect its changed title.
+   *
+   * @param  action (PageActions.Action, required)
+   *         The action to update.
+   */
+  updateActionTitle(action) {
+    let id = this._panelButtonNodeIDForActionID(action.id);
+    let node = document.getElementById(id);
+    if (node) {
+      node.setAttribute("label", action.title);
+    }
+  },
+
+  /**
+   * Returns the action for a node.
+   *
+   * @param  node (DOM node, required)
+   *         A button DOM node, either one that's shown in the page action panel
+   *         or the urlbar.
+   * @return (PageAction.Action) The node's related action, or null if none.
+   */
+  actionForNode(node) {
+    if (!node) {
+      return null;
+    }
+    let actionID = this._actionIDForNodeID(node.id);
+    return PageActions.actionForID(actionID);
+  },
+
+  // The ID of the given action's top-level button in the panel.
+  _panelButtonNodeIDForActionID(actionID) {
+    return `pageAction-panel-${actionID}`;
+  },
+
+  // The ID of the given action's button in the urlbar.
+  _urlbarButtonNodeIDForActionID(actionID) {
+    let action = PageActions.actionForID(actionID);
+    if (action && action.urlbarIDOverride) {
+      return action.urlbarIDOverride;
+    }
+    return `pageAction-urlbar-${actionID}`;
+  },
+
+  // The ID of the given action's panelview.
+  _panelViewNodeIDForActionID(actionID, forUrlbar) {
+    let placementID = forUrlbar ? "urlbar" : "panel";
+    return `pageAction-${placementID}-${actionID}-subview`;
+  },
+
+  // The ID of the given button in the given action's panelview.
+  _panelViewButtonNodeIDForActionID(actionID, buttonID, forUrlbar) {
+    let placementID = forUrlbar ? "urlbar" : "panel";
+    return `pageAction-${placementID}-${actionID}-${buttonID}`;
+  },
+
+  // The ID of the action corresponding to the given top-level button in the
+  // panel or button in the urlbar.
+  _actionIDForNodeID(nodeID) {
+    if (!nodeID) {
+      return null;
+    }
+    let match = nodeID.match(/^pageAction-(?:panel|urlbar)-(.+)$/);
+    return match ? match[1] : null;
+  },
+
+  /**
+   * Call this when the main page action button in the urlbar is activated.
+   *
+   * @param  event (DOM event, required)
+   *         The click or whatever event.
+   */
+  mainButtonClicked(event) {
+    event.stopPropagation();
+    if ((event.type == "click" && event.button != 0) ||
+        (event.type == "keypress" && event.charCode != KeyEvent.DOM_VK_SPACE &&
+         event.keyCode != KeyEvent.DOM_VK_RETURN)) {
+      return;
+    }
+
+    // If the temp panel is open and anchored to the main button, close it.
+    let tempPanel = document.getElementById(this._tempPanelID);
+    if (tempPanel && tempPanel.anchorNode.id == this.mainButtonNode.id) {
+      tempPanel.hidePopup();
+      return;
+    }
+
+    for (let action of PageActions.actions) {
+      let buttonNodeID = this._panelButtonNodeIDForActionID(action.id);
+      let buttonNode = document.getElementById(buttonNodeID);
+      action.onShowingInPanel(buttonNode);
+    }
+
+    this.panelNode.hidden = false;
+    this.panelNode.openPopup(this.mainButtonNode, {
+      position: "bottomcenter topright",
+      triggerEvent: event,
+    });
+  },
+
+  /**
+   * Call this on the contextmenu event.  Note that this is called before
+   * onContextMenuShowing.
+   *
+   * @param  event (DOM event, required)
+   *         The contextmenu event.
+   */
+  onContextMenu(event) {
+    let node = event.originalTarget;
+    this._contextAction = this.actionForNode(node);
+  },
+
+  /**
+   * Call this on the context menu's popupshowing event.
+   *
+   * @param  event (DOM event, required)
+   *         The popupshowing event.
+   * @param  popup (DOM node, required)
+   *         The context menu popup DOM node.
+   */
+  onContextMenuShowing(event, popup) {
+    if (event.target != popup) {
+      return;
+    }
+    // Right now there's only one item in the context menu, to toggle the
+    // context action's shown-in-urlbar state.  Update it now.
+    let toggleItem = popup.firstChild;
+    let toggleItemLabel = null;
+    if (this._contextAction) {
+      toggleItem.disabled = false;
+      if (this._contextAction.shownInUrlbar) {
+        toggleItemLabel = toggleItem.getAttribute("remove-label");
+      }
+    }
+    if (!toggleItemLabel) {
+      toggleItemLabel = toggleItem.getAttribute("add-label");
+    }
+    toggleItem.label = toggleItemLabel;
+  },
+
+  /**
+   * Call this from the context menu's toggle menu item.
+   */
+  toggleShownInUrlbarForContextAction() {
+    if (!this._contextAction) {
+      return;
+    }
+    this._contextAction.shownInUrlbar = !this._contextAction.shownInUrlbar;
+  },
+
+  _contextAction: null,
+
+  /**
+   * A bunch of strings (labels for actions and the like) are defined in DTD,
+   * but actions are created in JS.  So what we do is add a bunch of attributes
+   * to the page action panel's definition in the markup, whose values hold
+   * these DTD strings.  Then when each built-in action is set up, we get the
+   * related strings from the panel node and set up the action's node with them.
+   *
+   * The convention is to set for example the "title" property in an action's JS
+   * definition to the name of the attribute on the panel node that holds the
+   * actual title string.  Then call this function, passing the action's related
+   * DOM node and the name of the attribute that you are setting on the DOM
+   * node -- "label" or "title" in this example (either will do).
+   *
+   * @param  node (DOM node, required)
+   *         The node of an action you're setting up.
+   * @param  attrName (string, required)
+   *         The name of the attribute *on the node you're setting up*.
+   */
+  takeNodeAttributeFromPanel(node, attrName) {
+    let panelAttrName = node.getAttribute(attrName);
+    if (!panelAttrName && attrName == "title") {
+      attrName = "label";
+      panelAttrName = node.getAttribute(attrName);
+    }
+    if (panelAttrName) {
+      let attrValue = this.panelNode.getAttribute(panelAttrName);
+      if (attrValue) {
+        node.setAttribute(attrName, attrValue);
+      }
+    }
+  },
+};
+
+
+// built-in actions below //////////////////////////////////////////////////////
+
+// bookmark
+BrowserPageActions.bookmark = {
+  onShowingInPanel(buttonNode) {
+    // Update the button label via the bookmark observer.
+    BookmarkingUI.updateBookmarkPageMenuItem();
+  },
+
+  onCommand(event, buttonNode) {
+    BrowserPageActions.panelNode.hidePopup();
+    BookmarkingUI.onStarCommand(event);
+  },
+};
+
+// copy URL
+BrowserPageActions.copyURL = {
+  onPlacedInPanel(buttonNode) {
+    BrowserPageActions.takeNodeAttributeFromPanel(buttonNode, "title");
+  },
+
+  onCommand(event, buttonNode) {
+    BrowserPageActions.panelNode.hidePopup();
+    Cc["@mozilla.org/widget/clipboardhelper;1"]
+      .getService(Ci.nsIClipboardHelper)
+      .copyString(gBrowser.selectedBrowser.currentURI.spec);
+  },
+};
+
+// email link
+BrowserPageActions.emailLink = {
+  onPlacedInPanel(buttonNode) {
+    BrowserPageActions.takeNodeAttributeFromPanel(buttonNode, "title");
+  },
+
+  onCommand(event, buttonNode) {
+    BrowserPageActions.panelNode.hidePopup();
+    MailIntegration.sendLinkForBrowser(gBrowser.selectedBrowser);
+  },
+};
+
+// send to device
+BrowserPageActions.sendToDevice = {
+  onPlacedInPanel(buttonNode) {
+    BrowserPageActions.takeNodeAttributeFromPanel(buttonNode, "title");
+  },
+
+  onSubviewPlaced(panelViewNode) {
+    let bodyNode = panelViewNode.firstChild;
+    for (let node of bodyNode.childNodes) {
+      BrowserPageActions.takeNodeAttributeFromPanel(node, "title");
+      BrowserPageActions.takeNodeAttributeFromPanel(node, "shortcut");
+    }
+  },
+
+  onShowingInPanel(buttonNode) {
+    let browser = gBrowser.selectedBrowser;
+    let url = browser.currentURI.spec;
+    if (gSync.isSendableURI(url)) {
+      buttonNode.removeAttribute("disabled");
+    } else {
+      buttonNode.setAttribute("disabled", "true");
+    }
+  },
+
+  onShowingSubview(panelViewNode) {
+    let browser = gBrowser.selectedBrowser;
+    let url = browser.currentURI.spec;
+    let title = browser.contentTitle;
+
+    let bodyNode = panelViewNode.firstChild;
+
+    // This is on top because it also clears the device list between state
+    // changes.
+    gSync.populateSendTabToDevicesMenu(bodyNode, url, title, (clientId, name, clientType) => {
+      if (!name) {
+        return document.createElement("toolbarseparator");
+      }
+      let item = document.createElement("toolbarbutton");
+      item.classList.add("pageAction-sendToDevice-device", "subviewbutton");
+      if (clientId) {
+        item.classList.add("subviewbutton-iconic");
+      }
+      item.setAttribute("tooltiptext", name);
+      return item;
+    });
+
+    bodyNode.removeAttribute("state");
+    // In the first ~10 sec after startup, Sync may not be loaded and the list
+    // of devices will be empty.
+    if (gSync.syncConfiguredAndLoading) {
+      bodyNode.setAttribute("state", "notready");
+      // Force a background Sync
+      Services.tm.dispatchToMainThread(async () => {
+        await Weave.Service.sync([]);  // [] = clients engine only
+        // There's no way Sync is still syncing at this point, but we check
+        // anyway to avoid infinite looping.
+        if (!window.closed && !gSync.syncConfiguredAndLoading) {
+          this.onShowingSubview(panelViewNode);
+        }
+      });
+    }
+  },
+};
--- a/browser/base/content/browser-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -23,21 +23,16 @@
     <command id="cmd_newNavigatorTab" oncommand="BrowserOpenTab(event);"/>
     <command id="cmd_newNavigatorTabNoEvent" oncommand="BrowserOpenTab();"/>
     <command id="Browser:OpenFile"  oncommand="BrowserOpenFileWindow();"/>
     <command id="Browser:SavePage" oncommand="saveBrowser(gBrowser.selectedBrowser);"/>
 
     <command id="Browser:SendLink"
              oncommand="MailIntegration.sendLinkForBrowser(gBrowser.selectedBrowser);"/>
 
-    <command id="PageAction:copyURL"
-             oncommand="gPageActionButton.copyURL();"/>
-    <command id="PageAction:emailLink"
-             oncommand="gPageActionButton.emailLink();"/>
-
     <command id="cmd_pageSetup" oncommand="PrintUtils.showPageSetup();"/>
     <command id="cmd_print" oncommand="PrintUtils.printWindow(window.gBrowser.selectedBrowser.outerWindowID, window.gBrowser.selectedBrowser);"/>
     <command id="cmd_printPreview" oncommand="PrintUtils.printPreview(PrintPreviewListener);"/>
     <command id="cmd_close" oncommand="BrowserCloseTabOrWindow()"/>
     <command id="cmd_closeWindow" oncommand="BrowserTryToCloseWindow()"/>
     <command id="cmd_toggleMute" oncommand="gBrowser.selectedTab.toggleMuteAudio()"/>
     <command id="cmd_CustomizeToolbars" oncommand="BrowserCustomizeToolbar()"/>
     <command id="cmd_quitApplication" oncommand="goQuitApplication()"/>
--- a/browser/base/content/browser-sync.js
+++ b/browser/base/content/browser-sync.js
@@ -346,17 +346,17 @@ var gSync = {
 
   _appendSendTabDeviceList(fragment, createDeviceNodeFn, url, title) {
     const onTargetDeviceCommand = (event) => {
       let clients = event.target.getAttribute("clientId") ?
         [event.target.getAttribute("clientId")] :
         this.remoteClients.map(client => client.id);
 
       clients.forEach(clientId => this.sendTabToDevice(url, clientId, title));
-      gPageActionButton.panel.hidePopup();
+      BrowserPageActions.panelNode.hidePopup();
     }
 
     function addTargetDevice(clientId, name, clientType) {
       const targetDevice = createDeviceNodeFn(clientId, name, clientType);
       targetDevice.addEventListener("command", onTargetDeviceCommand, true);
       targetDevice.classList.add("sync-menuitem", "sendtab-target");
       targetDevice.setAttribute("clientId", clientId);
       targetDevice.setAttribute("clientType", clientType);
@@ -379,35 +379,35 @@ var gSync = {
     }
   },
 
   _appendSendTabSingleDevice(fragment, createDeviceNodeFn) {
     const noDevices = this.fxaStrings.GetStringFromName("sendTabToDevice.singledevice.status");
     const learnMore = this.fxaStrings.GetStringFromName("sendTabToDevice.singledevice");
     this._appendSendTabInfoItems(fragment, createDeviceNodeFn, noDevices, learnMore, () => {
       this.openSendToDevicePromo();
-      gPageActionButton.panel.hidePopup();
+      BrowserPageActions.panelNode.hidePopup();
     });
   },
 
   _appendSendTabVerify(fragment, createDeviceNodeFn) {
     const notVerified = this.fxaStrings.GetStringFromName("sendTabToDevice.verify.status");
     const verifyAccount = this.fxaStrings.GetStringFromName("sendTabToDevice.verify");
     this._appendSendTabInfoItems(fragment, createDeviceNodeFn, notVerified, verifyAccount, () => {
       this.openPrefs("sendtab");
-      gPageActionButton.panel.hidePopup();
+      BrowserPageActions.panelNode.hidePopup();
     });
   },
 
   _appendSendTabUnconfigured(fragment, createDeviceNodeFn) {
     const notConnected = this.fxaStrings.GetStringFromName("sendTabToDevice.unconfigured.status");
     const learnMore = this.fxaStrings.GetStringFromName("sendTabToDevice.unconfigured");
     this._appendSendTabInfoItems(fragment, createDeviceNodeFn, notConnected, learnMore, () => {
       this.openSendToDevicePromo();
-      gPageActionButton.panel.hidePopup();
+      BrowserPageActions.panelNode.hidePopup();
     });
   },
 
   _appendSendTabInfoItems(fragment, createDeviceNodeFn, statusLabel, actionLabel, actionCommand) {
     const status = createDeviceNodeFn(null, statusLabel, null);
     status.setAttribute("label", statusLabel);
     status.setAttribute("disabled", true);
     status.classList.add("sync-menuitem");
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -686,16 +686,17 @@ html|input.urlbar-input[textoverflow]:no
 }
 
 #DateTimePickerPanel[active="true"] {
   -moz-binding: url("chrome://global/content/bindings/datetimepopup.xml#datetime-popup");
 }
 
 #urlbar[pageproxystate="invalid"] > #urlbar-icons > .urlbar-icon,
 %ifdef MOZ_PHOTON_THEME
+#urlbar[pageproxystate="invalid"] > #urlbar-icons > #star-button-box > .urlbar-icon,
 .urlbar-go-button[pageproxystate="valid"],
 .urlbar-go-button:not([parentfocused="true"]),
 %else
 #urlbar[pageproxystate="invalid"][focused="true"] > #urlbar-go-button ~ toolbarbutton,
 #urlbar[pageproxystate="valid"] > #urlbar-go-button,
 #urlbar:not([focused="true"]) > #urlbar-go-button,
 %endif
 #urlbar[pageproxystate="invalid"] > #identity-box > #blocked-permissions-container,
@@ -1489,14 +1490,17 @@ toolbarpaletteitem[place="palette"][hidd
 }
 
 .dragfeedback-tab {
   -moz-appearance: none;
   opacity: 0.65;
   -moz-window-shadow: none;
 }
 
-/* Page action menu */
-#page-action-sendToDeviceView-body:not([state="notready"]) > #page-action-sync-not-ready-button {
+%ifdef MOZ_PHOTON_THEME
+/* Page action panel */
+#pageAction-panel-sendToDevice-subview-body:not([state="notready"]) > #pageAction-panel-sendToDevice-notReady,
+#pageAction-urlbar-sendToDevice-subview-body:not([state="notready"]) > #pageAction-urlbar-sendToDevice-notReady {
   display: none;
 }
+%endif
 
 %include theme-vars.inc.css
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -36,17 +36,17 @@ XPCOMUtils.defineLazyPreferenceGetter(th
           ReaderParent:false, RecentWindow:false, SafeBrowsing: false,
           SessionStore:false,
           ShortcutUtils:false, SimpleServiceDiscovery:false, SitePermissions:false,
           Social:false, TabCrashHandler:false, TelemetryStopwatch:false,
           Translation:false, UITour:false, Utils:false, UpdateUtils:false,
           Weave:false,
           WebNavigationFrames: false, fxAccounts:false, gDevTools:false,
           gDevToolsBrowser:false, webrtcUI:false, ZoomUI:false,
-          Marionette:false,
+          Marionette:false, PageActions:false,
  */
 
 /**
  * IF YOU ADD OR REMOVE FROM THIS LIST, PLEASE UPDATE THE LIST ABOVE AS WELL.
  * XXX Bug 1325373 is for making eslint detect these automatically.
  */
 [
   ["AboutHome", "resource:///modules/AboutHome.jsm"],
@@ -64,16 +64,17 @@ XPCOMUtils.defineLazyPreferenceGetter(th
   ["E10SUtils", "resource:///modules/E10SUtils.jsm"],
   ["ExtensionsUI", "resource:///modules/ExtensionsUI.jsm"],
   ["FormValidationHandler", "resource:///modules/FormValidationHandler.jsm"],
   ["GMPInstallManager", "resource://gre/modules/GMPInstallManager.jsm"],
   ["LightweightThemeManager", "resource://gre/modules/LightweightThemeManager.jsm"],
   ["Log", "resource://gre/modules/Log.jsm"],
   ["LoginManagerParent", "resource://gre/modules/LoginManagerParent.jsm"],
   ["NewTabUtils", "resource://gre/modules/NewTabUtils.jsm"],
+  ["PageActions", "resource:///modules/PageActions.jsm"],
   ["PageThumbs", "resource://gre/modules/PageThumbs.jsm"],
   ["PluralForm", "resource://gre/modules/PluralForm.jsm"],
   ["Preferences", "resource://gre/modules/Preferences.jsm"],
   ["PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"],
   ["ProcessHangMonitor", "resource:///modules/ProcessHangMonitor.jsm"],
   ["PromiseUtils", "resource://gre/modules/PromiseUtils.jsm"],
   ["ReaderMode", "resource://gre/modules/ReaderMode.jsm"],
   ["ReaderParent", "resource:///modules/ReaderParent.jsm"],
@@ -1385,16 +1386,17 @@ var gBrowserInit = {
       gURLBar.setAttribute("readonly", "true");
       gURLBar.setAttribute("enablehistory", "false");
     }
 
     // Misc. inits.
     TabletModeUpdater.init();
     CombinedStopReload.init();
     gPrivateBrowsingUI.init();
+    BrowserPageActions.init();
 
     if (window.matchMedia("(-moz-os-version: windows-win8)").matches &&
         window.matchMedia("(-moz-windows-default-theme)").matches) {
       let windowFrameColor = new Color(...Cu.import("resource:///modules/Windows8WindowFrameColor.jsm", {})
                                             .Windows8WindowFrameColor.get());
       // Default to black for foreground text.
       if (!windowFrameColor.isContrastRatioAcceptable(new Color(0, 0, 0))) {
         document.documentElement.setAttribute("darkwindowframe", "true");
@@ -7979,116 +7981,16 @@ var gIdentityHandler = {
     container.appendChild(nameLabel);
     container.appendChild(stateLabel);
     container.appendChild(button);
 
     return container;
   }
 };
 
-var gPageActionButton = {
-  get button() {
-    delete this.button;
-    return this.button = document.getElementById("urlbar-page-action-button");
-  },
-
-  get panel() {
-    delete this.panel;
-    return this.panel = document.getElementById("page-action-panel");
-  },
-
-  get sendToDeviceBody() {
-    delete this.sendToDeviceBody;
-    return this.sendToDeviceBody = document.getElementById("page-action-sendToDeviceView-body");
-  },
-
-  onEvent(event) {
-    event.stopPropagation();
-
-    if ((event.type == "click" && event.button != 0) ||
-        (event.type == "keypress" && event.charCode != KeyEvent.DOM_VK_SPACE &&
-         event.keyCode != KeyEvent.DOM_VK_RETURN)) {
-      return; // Left click, space or enter only
-    }
-
-    this._preparePanelToBeShown();
-    this.panel.hidden = false;
-    this.panel.openPopup(this.button, {
-      position: "bottomcenter topright",
-      triggerEvent: event,
-    });
-  },
-
-  _preparePanelToBeShown() {
-    // Update the bookmark item's label.
-    BookmarkingUI.updateBookmarkPageMenuItem();
-
-    // Update the send-to-device item's disabled state.
-    let browser = gBrowser.selectedBrowser;
-    let url = browser.currentURI.spec;
-    let sendToDeviceItem =
-      document.getElementById("page-action-send-to-device-button");
-    sendToDeviceItem.disabled = !gSync.isSendableURI(url);
-  },
-
-  copyURL() {
-    this.panel.hidePopup();
-    Cc["@mozilla.org/widget/clipboardhelper;1"]
-      .getService(Ci.nsIClipboardHelper)
-      .copyString(gBrowser.selectedBrowser.currentURI.spec);
-  },
-
-  emailLink() {
-    this.panel.hidePopup();
-    MailIntegration.sendLinkForBrowser(gBrowser.selectedBrowser);
-  },
-
-  showSendToDeviceView(subviewButton) {
-    this.setupSendToDeviceView();
-    PanelUI.showSubView("page-action-sendToDeviceView", subviewButton);
-  },
-
-  setupSendToDeviceView() {
-    let browser = gBrowser.selectedBrowser;
-    let url = browser.currentURI.spec;
-    let title = browser.contentTitle;
-    let body = this.sendToDeviceBody;
-
-    // This is on top because it also clears the device list between state changes.
-    gSync.populateSendTabToDevicesMenu(body, url, title, (clientId, name, clientType) => {
-      if (!name) {
-        return document.createElement("toolbarseparator");
-      }
-      let item = document.createElement("toolbarbutton");
-      item.classList.add("page-action-sendToDevice-device", "subviewbutton");
-      if (clientId) {
-        item.classList.add("subviewbutton-iconic");
-      }
-      item.setAttribute("tooltiptext", name);
-      return item;
-    });
-
-    body.removeAttribute("state");
-    // In the first ~10 sec after startup, Sync may not be loaded and the list
-    // of devices will be empty.
-    if (gSync.syncConfiguredAndLoading) {
-      body.setAttribute("state", "notready");
-      // Force a background Sync
-      Services.tm.dispatchToMainThread(async () => {
-        await Weave.Service.sync([]);  // [] = clients engine only
-        // There's no way Sync is still syncing at this point, but we check
-        // anyway to avoid infinite looping.
-        if (!window.closed && !gSync.syncConfiguredAndLoading) {
-          this.setupSendToDeviceView();
-        }
-      });
-    }
-  },
-};
-
 /**
  * Fired on the "marionette-remote-control" system notification,
  * indicating if the browser session is under remote control.
  */
 const gRemoteControl = {
   observe(subject, topic, data) {
     gRemoteControl.updateVisualCue(data);
   },
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -437,64 +437,52 @@
         <button class="ctrlTab-preview" flex="1"/>
         <button class="ctrlTab-preview" flex="1"/>
       </hbox>
       <hbox pack="center">
         <button id="ctrlTab-showAll" class="ctrlTab-preview" noicon="true"/>
       </hbox>
     </panel>
 
-    <panel id="page-action-panel"
+#ifdef MOZ_PHOTON_THEME
+    <panel id="pageActionPanel"
            class="cui-widget-panel"
            role="group"
            type="arrow"
            hidden="true"
            flip="slide"
            position="bottomcenter topright"
            tabspecific="true"
-           noautofocus="true">
-      <photonpanelmultiview id="page-action-multiView"
-                            mainViewId="page-action-mainView">
-        <panelview id="page-action-mainView"
+           noautofocus="true"
+           context="pageActionPanelContextMenu"
+           oncontextmenu="BrowserPageActions.onContextMenu(event);"
+           copyURL-title="&copyURLCmd.label;"
+           emailLink-title="&emailPageCmd.label;"
+           sendToDevice-title="&sendToDevice.label2;"
+           sendToDevice-notReadyTitle="&sendToDevice.syncNotReady.label;">
+      <photonpanelmultiview id="pageActionPanelMultiView"
+                            mainViewId="pageActionPanelMainView"
+                            viewCacheId="appMenu-viewCache">
+        <panelview id="pageActionPanelMainView"
                    class="PanelUI-subView">
-          <vbox class="panel-subview-body">
-            <toolbarbutton id="page-action-bookmark-button"
-                           class="subviewbutton subviewbutton-iconic"
-                           observes="bookmarkThisPageBroadcaster"
-                           command="Browser:AddBookmarkAs"
-                           onclick="gPageActionButton.panel.hidePopup();"/>
-            <toolbarseparator/>
-            <toolbarbutton id="page-action-copy-url-button"
-                           class="subviewbutton subviewbutton-iconic"
-                           label="&copyURLCmd.label;"
-                           command="PageAction:copyURL"/>
-            <toolbarbutton id="page-action-email-link-button"
-                           class="subviewbutton subviewbutton-iconic"
-                           label="&emailPageCmd.label;"
-                           command="PageAction:emailLink"/>
-            <toolbarbutton id="page-action-send-to-device-button"
-                           class="subviewbutton subviewbutton-iconic subviewbutton-nav"
-                           label="&sendToDevice.label2;"
-                           closemenu="none"
-                           oncommand="gPageActionButton.showSendToDeviceView(this);"/>
-          </vbox>
-        </panelview>
-        <panelview id="page-action-sendToDeviceView"
-                   class="PanelUI-subView"
-                   title="&sendToDevice.viewTitle;">
-          <vbox id="page-action-sendToDeviceView-body" class="panel-subview-body">
-            <toolbarbutton id="page-action-sync-not-ready-button"
-                           class="subviewbutton"
-                           label="&sendToDevice.syncNotReady.label;"
-                           disabled="true"/>
-          </vbox>
+          <vbox class="panel-subview-body"/>
         </panelview>
       </photonpanelmultiview>
     </panel>
 
+    <menupopup id="pageActionPanelContextMenu"
+               onpopupshowing="BrowserPageActions.onContextMenuShowing(event, this);">
+      <menuitem id="pageActionPanelContextMenu-toggleUrlbar"
+                add-label="&pageAction.addToUrlbar.label;"
+                remove-label="&pageAction.removeFromUrlbar.label;"
+                label="&pageAction.addToUrlbar.label;"
+                oncommand="BrowserPageActions.toggleShownInUrlbarForContextAction();"/>
+    </menupopup>
+#endif
+
     <!-- Bookmarks and history tooltip -->
     <tooltip id="bhTooltip"/>
 
     <tooltip id="tabbrowser-tab-tooltip" onpopupshowing="gBrowser.createTooltip(event);"/>
 
     <tooltip id="back-button-tooltip">
       <label class="tooltip-label" value="&backButton.tooltip;"/>
 #ifdef XP_MACOSX
@@ -899,34 +887,38 @@
                        class="urlbar-icon"
                        hidden="true"
                        tooltiptext="&pageReportIcon.tooltip;"
                        onmousedown="gPopupBlockerObserver.onReportButtonMousedown(event);"/>
                 <image id="reader-mode-button"
                        class="urlbar-icon"
                        hidden="true"
                        onclick="ReaderParent.buttonClick(event);"/>
+                <toolbarbutton id="urlbar-zoom-button"
+                       onclick="FullZoom.reset();"
+                       tooltip="dynamic-shortcut-tooltip"
+                       hidden="true"/>
 #ifdef MOZ_PHOTON_THEME
-                <hbox id="star-button-box">
+                <image id="pageActionButton"
+                       class="urlbar-icon"
+                       tooltiptext="&pageActionButton.tooltip;"
+                       onclick="BrowserPageActions.mainButtonClicked(event);"/>
+                <hbox id="star-button-box" hidden="true">
                   <image id="star-button"
                          class="urlbar-icon"
                          onclick="BookmarkingUI.onStarCommand(event);">
                     <observes element="bookmarkThisPageBroadcaster" attribute="starred"/>
                     <observes element="bookmarkThisPageBroadcaster" attribute="tooltiptext"/>
                   </image>
                   <hbox id="star-button-animatable-box">
                     <image id="star-button-animatable-image"
                            onclick="BookmarkingUI.onStarCommand(event);"/>
                   </hbox>
                 </hbox>
 #endif
-                <toolbarbutton id="urlbar-zoom-button"
-                       onclick="FullZoom.reset();"
-                       tooltip="dynamic-shortcut-tooltip"
-                       hidden="true"/>
               </hbox>
               <hbox id="userContext-icons" hidden="true">
                 <label id="userContext-label"/>
                 <image id="userContext-indicator"/>
               </hbox>
 #ifndef MOZ_PHOTON_THEME
               <toolbarbutton id="urlbar-go-button"
                              class="chromeclass-toolbar-additional"
@@ -936,21 +928,16 @@
                              class="chromeclass-toolbar-additional"
                              command="Browser:ReloadOrDuplicate"
                              onclick="checkForMiddleClick(this, event);"
                              tooltip="dynamic-shortcut-tooltip"/>
               <toolbarbutton id="stop-button"
                              class="chromeclass-toolbar-additional"
                              command="Browser:Stop"
                              tooltip="dynamic-shortcut-tooltip"/>
-#else
-              <toolbarbutton id="urlbar-page-action-button"
-                             class="chromeclass-toolbar-additional"
-                             tooltiptext="&pageActionButton.tooltip;"
-                             onclick="gPageActionButton.onEvent(event);"/>
 #endif
             </textbox>
           </hbox>
         </toolbaritem>
 
         <toolbaritem id="search-container" title="&searchItem.title;"
                      align="center" class="chromeclass-toolbar-additional panel-wide-item"
                      cui-areatype="toolbar"
@@ -1087,19 +1074,21 @@
                        cui-areatype="toolbar"
                        aboutHomeOverrideTooltip="&abouthome.pageTitle;"/>
       </hbox>
 
       <toolbarbutton id="nav-bar-overflow-button"
                      class="toolbarbutton-1 chromeclass-toolbar-additional overflow-button"
                      skipintoolbarset="true"
                      tooltiptext="&navbarOverflow.label;">
+#ifdef MOZ_PHOTON_ANIMATIONS
         <box class="toolbarbutton-animatable-box">
           <image class="toolbarbutton-animatable-image"/>
         </box>
+#endif
       </toolbarbutton>
 
       <toolbaritem id="PanelUI-button"
                    class="chromeclass-toolbar-additional"
                    removable="false">
         <toolbarbutton id="PanelUI-menu-button"
                        class="toolbarbutton-1 badged-button"
                        consumeanchor="PanelUI-button"
--- a/browser/base/content/global-scripts.inc
+++ b/browser/base/content/global-scripts.inc
@@ -8,16 +8,17 @@
 # tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js
 
 <script type="application/javascript" src="chrome://browser/content/browser.js"/>
 
 <script type="application/javascript" src="chrome://browser/content/browser-captivePortal.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-compacttheme.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-feeds.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-media.js"/>
+<script type="application/javascript" src="chrome://browser/content/browser-pageActions.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-places.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-plugins.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-sidebar.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-tabsintitlebar.js"/>
 <script type="application/javascript" src="chrome://browser/content/browser-trackingprotection.js"/>
 
 #ifdef MOZ_DATA_REPORTING
 <script type="application/javascript" src="chrome://browser/content/browser-data-submission-info-bar.js"/>
--- a/browser/base/content/test/performance/browser.ini
+++ b/browser/base/content/test/performance/browser.ini
@@ -9,11 +9,13 @@ skip-if = !e10s
 [browser_startup_images.js]
 skip-if = !debug
 [browser_tabclose_grow_reflows.js]
 [browser_tabclose_reflows.js]
 [browser_tabopen_reflows.js]
 [browser_tabopen_squeeze_reflows.js]
 [browser_tabswitch_reflows.js]
 [browser_toolbariconcolor_restyles.js]
+[browser_urlbar_search_reflows.js]
+skip-if = (os == 'linux') || (os == 'mac' && !debug) # Disabled on Linux and OS X opt due to frequent failures. Bug 1385932 and Bug 1384582
 [browser_windowclose_reflows.js]
 [browser_windowopen_reflows.js]
 skip-if = os == 'linux' # Disabled due to frequent failures. Bug 1380465.
--- a/browser/base/content/test/performance/browser_appmenu_reflows.js
+++ b/browser/base/content/test/performance/browser_appmenu_reflows.js
@@ -5,99 +5,79 @@
  * EXPECTED_APPMENU_OPEN_REFLOWS. This is a whitelist that should slowly go
  * away as we improve the performance of the front-end. Instead of adding more
  * reflows to the whitelist, you should be modifying your code to avoid the reflow.
  *
  * See https://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
  * for tips on how to do that.
  */
 const EXPECTED_APPMENU_OPEN_REFLOWS = [
-  [
-    "openPopup@chrome://global/content/bindings/popup.xml",
-    "show/</<@chrome://browser/content/customizableui/panelUI.js",
-  ],
-
-  [
-    "get_alignmentPosition@chrome://global/content/bindings/popup.xml",
-    "adjustArrowPosition@chrome://global/content/bindings/popup.xml",
-    "onxblpopupshowing@chrome://global/content/bindings/popup.xml",
-    "openPopup@chrome://global/content/bindings/popup.xml",
-    "show/</<@chrome://browser/content/customizableui/panelUI.js",
-  ],
+  {
+    stack: [
+      "openPopup@chrome://global/content/bindings/popup.xml",
+      "show/</<@chrome://browser/content/customizableui/panelUI.js",
+    ],
+  },
 
-  [
-    "get_alignmentPosition@chrome://global/content/bindings/popup.xml",
-    "adjustArrowPosition@chrome://global/content/bindings/popup.xml",
-    "onxblpopupshowing@chrome://global/content/bindings/popup.xml",
-    "openPopup@chrome://global/content/bindings/popup.xml",
-    "show/</<@chrome://browser/content/customizableui/panelUI.js",
-  ],
+  {
+    stack: [
+      "get_alignmentPosition@chrome://global/content/bindings/popup.xml",
+      "adjustArrowPosition@chrome://global/content/bindings/popup.xml",
+      "onxblpopupshowing@chrome://global/content/bindings/popup.xml",
+      "openPopup@chrome://global/content/bindings/popup.xml",
+      "show/</<@chrome://browser/content/customizableui/panelUI.js",
+    ],
 
-  [
-    "get_alignmentPosition@chrome://global/content/bindings/popup.xml",
-    "adjustArrowPosition@chrome://global/content/bindings/popup.xml",
-    "onxblpopuppositioned@chrome://global/content/bindings/popup.xml",
-  ],
+    times: 2, // This number should only ever go down - never up.
+  },
 
-  [
-    "get_alignmentPosition@chrome://global/content/bindings/popup.xml",
-    "handleEvent@resource:///modules/PanelMultiView.jsm",
-    "openPopup@chrome://global/content/bindings/popup.xml",
-  ],
-
-  [
-    "handleEvent@resource:///modules/PanelMultiView.jsm",
-    "openPopup@chrome://global/content/bindings/popup.xml",
-  ],
-
-  [
-    "handleEvent@resource:///modules/PanelMultiView.jsm",
-    "openPopup@chrome://global/content/bindings/popup.xml",
-  ],
+  {
+    stack: [
+      "get_alignmentPosition@chrome://global/content/bindings/popup.xml",
+      "adjustArrowPosition@chrome://global/content/bindings/popup.xml",
+      "onxblpopuppositioned@chrome://global/content/bindings/popup.xml",
+    ],
+  },
 
-  [
-    "handleEvent@resource:///modules/PanelMultiView.jsm",
-    "openPopup@chrome://global/content/bindings/popup.xml",
-  ],
-
-  [
-    "handleEvent@resource:///modules/PanelMultiView.jsm",
-    "openPopup@chrome://global/content/bindings/popup.xml",
-  ],
+  {
+    stack: [
+      "get_alignmentPosition@chrome://global/content/bindings/popup.xml",
+      "handleEvent@resource:///modules/PanelMultiView.jsm",
+      "openPopup@chrome://global/content/bindings/popup.xml",
+    ],
+  },
 
-  [
-    "handleEvent@resource:///modules/PanelMultiView.jsm",
-    "openPopup@chrome://global/content/bindings/popup.xml",
-  ],
+  {
+    stack: [
+      "handleEvent@resource:///modules/PanelMultiView.jsm",
+      "openPopup@chrome://global/content/bindings/popup.xml",
+    ],
 
-  [
-    "handleEvent@resource:///modules/PanelMultiView.jsm",
-    "openPopup@chrome://global/content/bindings/popup.xml",
-  ],
+    times: 6, // This number should only ever go down - never up.
+  },
 ];
 
 const EXPECTED_APPMENU_SUBVIEW_REFLOWS = [
   /**
    * The synced tabs view has labels that are multiline. Because of bugs in
    * XUL layout relating to multiline text in scrollable containers, we need
    * to manually read their height in order to ensure container heights are
    * correct. Unfortunately this requires 2 sync reflows.
    *
    * If we add more views where this is necessary, we may need to duplicate
    * these expected reflows further.
    */
-  [
-    "descriptionHeightWorkaround@resource:///modules/PanelMultiView.jsm",
-    "onTransitionEnd@resource:///modules/PanelMultiView.jsm",
-  ],
+  {
+    stack: [
+      "descriptionHeightWorkaround@resource:///modules/PanelMultiView.jsm",
+      "onTransitionEnd@resource:///modules/PanelMultiView.jsm",
+    ],
 
-  [
-    "descriptionHeightWorkaround@resource:///modules/PanelMultiView.jsm",
-    "onTransitionEnd@resource:///modules/PanelMultiView.jsm",
-  ],
+    times: 2, // This number should only ever go down - never up.
+  },
 
   /**
    * Please don't add anything new!
    */
 ];
 
 add_task(async function() {
   await ensureNoPreloadedBrowser();
--- a/browser/base/content/test/performance/browser_startup_images.js
+++ b/browser/base/content/test/performance/browser_startup_images.js
@@ -25,16 +25,26 @@
  */
 const whitelist = [
   // Photon-only entries
   {
     file: "chrome://browser/skin/stop.svg",
     platforms: ["linux", "win", "macosx"],
     photon: true,
   },
+  {
+    file: "chrome://browser/skin/bookmark-hollow.svg",
+    platforms: ["linux", "win", "macosx"],
+    photon: true,
+  },
+  {
+    file: "chrome://browser/skin/page-action.svg",
+    platforms: ["linux", "win", "macosx"],
+    photon: true,
+  },
 
   // Non-Photon-only entries
   {
     file: "chrome://browser/skin/toolbarbutton-dropdown-arrow.png",
     platforms: ["linux", "win", "macosx"],
     photon: false,
   },
 
--- a/browser/base/content/test/performance/browser_tabopen_reflows.js
+++ b/browser/base/content/test/performance/browser_tabopen_reflows.js
@@ -10,19 +10,21 @@
  * be modifying your code to avoid the reflow.
  *
  * See https://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
  * for tips on how to do that.
  */
 const EXPECTED_REFLOWS = [
   // selection change notification may cause querying the focused editor content
   // by IME and that will cause reflow.
-  [
-    "select@chrome://global/content/bindings/textbox.xml",
-  ],
+  {
+    stack: [
+      "select@chrome://global/content/bindings/textbox.xml",
+    ],
+  }
 ];
 
 /*
  * This test ensures that there are no unexpected
  * uninterruptible reflows when opening new tabs.
  */
 add_task(async function() {
   await ensureNoPreloadedBrowser();
--- a/browser/base/content/test/performance/browser_tabopen_squeeze_reflows.js
+++ b/browser/base/content/test/performance/browser_tabopen_squeeze_reflows.js
@@ -5,21 +5,23 @@
  * is a whitelist that should slowly go away as we improve the performance of
  * the front-end. Instead of adding more reflows to the whitelist, you should
  * be modifying your code to avoid the reflow.
  *
  * See https://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
  * for tips on how to do that.
  */
 const EXPECTED_REFLOWS = [
-  [
-    "select@chrome://global/content/bindings/textbox.xml",
-    "focusAndSelectUrlBar@chrome://browser/content/browser.js",
-    "_adjustFocusAfterTabSwitch@chrome://browser/content/tabbrowser.xml",
-  ],
+  {
+    stack: [
+      "select@chrome://global/content/bindings/textbox.xml",
+      "focusAndSelectUrlBar@chrome://browser/content/browser.js",
+      "_adjustFocusAfterTabSwitch@chrome://browser/content/tabbrowser.xml",
+    ],
+  }
 ];
 
 /*
  * This test ensures that there are no unexpected
  * uninterruptible reflows when opening a new tab that will
  * cause the existing tabs to squeeze smaller.
  */
 add_task(async function() {
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/performance/browser_urlbar_search_reflows.js
@@ -0,0 +1,268 @@
+"use strict";
+
+// There are a _lot_ of reflows in this test, and processing them takes
+// time. On slower builds, we need to boost our allowed test time.
+requestLongerTimeout(5);
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
+                                  "resource://testing-common/PlacesTestUtils.jsm");
+
+/**
+ * WHOA THERE: We should never be adding new things to EXPECTED_REFLOWS. This
+ * is a whitelist that should slowly go away as we improve the performance of
+ * the front-end. Instead of adding more reflows to the whitelist, you should
+ * be modifying your code to avoid the reflow.
+ *
+ * See https://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
+ * for tips on how to do that.
+ */
+
+/* These reflows happen only the first time the awesomebar panel opens. */
+const EXPECTED_REFLOWS_FIRST_OPEN = [
+  // Bug 1357054
+  {
+    stack: [
+      "_rebuild@chrome://browser/content/search/search.xml",
+      "set_popup@chrome://browser/content/search/search.xml",
+      "enableOneOffSearches@chrome://browser/content/urlbarBindings.xml",
+      "_enableOrDisableOneOffSearches@chrome://browser/content/urlbarBindings.xml",
+      "urlbar_XBL_Constructor/<@chrome://browser/content/urlbarBindings.xml",
+      "openPopup@chrome://global/content/bindings/popup.xml",
+      "_openAutocompletePopup@chrome://browser/content/urlbarBindings.xml",
+      "openAutocompletePopup@chrome://browser/content/urlbarBindings.xml",
+      "openPopup@chrome://global/content/bindings/autocomplete.xml",
+      "set_popupOpen@chrome://global/content/bindings/autocomplete.xml"
+    ],
+    times: 1, // This number should only ever go down - never up.
+  },
+
+  {
+    stack: [
+      "adjustSiteIconStart@chrome://global/content/bindings/autocomplete.xml",
+      "set_siteIconStart@chrome://global/content/bindings/autocomplete.xml",
+      "_openAutocompletePopup@chrome://browser/content/urlbarBindings.xml",
+      "openAutocompletePopup@chrome://browser/content/urlbarBindings.xml",
+      "openPopup@chrome://global/content/bindings/autocomplete.xml",
+      "set_popupOpen@chrome://global/content/bindings/autocomplete.xml",
+    ],
+  },
+
+  {
+    stack: [
+      "adjustSiteIconStart@chrome://global/content/bindings/autocomplete.xml",
+      "_reuseAcItem@chrome://global/content/bindings/autocomplete.xml",
+      "_appendCurrentResult@chrome://global/content/bindings/autocomplete.xml",
+      "_invalidate@chrome://global/content/bindings/autocomplete.xml",
+      "invalidate@chrome://global/content/bindings/autocomplete.xml",
+    ],
+    times: 9, // This number should only ever go down - never up.
+  },
+
+  {
+    stack: [
+      "adjustHeight@chrome://global/content/bindings/autocomplete.xml",
+      "onxblpopupshown@chrome://global/content/bindings/autocomplete.xml"
+    ],
+    times: 5, // This number should only ever go down - never up.
+  },
+
+  {
+    stack: [
+      "_handleOverflow@chrome://global/content/bindings/autocomplete.xml",
+      "handleOverUnderflow@chrome://global/content/bindings/autocomplete.xml",
+      "set_siteIconStart@chrome://global/content/bindings/autocomplete.xml",
+    ],
+    times: 6, // This number should only ever go down - never up.
+  },
+
+  {
+    stack: [
+      "adjustHeight@chrome://global/content/bindings/autocomplete.xml",
+      "_invalidate/this._adjustHeightTimeout<@chrome://global/content/bindings/autocomplete.xml",
+    ],
+    times: 3, // This number should only ever go down - never up.
+  },
+
+  {
+    stack: [
+      "_handleOverflow@chrome://global/content/bindings/autocomplete.xml",
+      "handleOverUnderflow@chrome://global/content/bindings/autocomplete.xml",
+      "_reuseAcItem@chrome://global/content/bindings/autocomplete.xml",
+      "_appendCurrentResult@chrome://global/content/bindings/autocomplete.xml",
+      "_invalidate@chrome://global/content/bindings/autocomplete.xml",
+      "invalidate@chrome://global/content/bindings/autocomplete.xml"
+    ],
+    times: 390, // This number should only ever go down - never up.
+  },
+
+  {
+    stack: [
+      "_openAutocompletePopup@chrome://browser/content/urlbarBindings.xml",
+      "openAutocompletePopup@chrome://browser/content/urlbarBindings.xml",
+      "openPopup@chrome://global/content/bindings/autocomplete.xml",
+      "set_popupOpen@chrome://global/content/bindings/autocomplete.xml",
+    ],
+    times: 3, // This number should only ever go down - never up.
+  },
+
+  // Bug 1359989
+  {
+    stack: [
+      "openPopup@chrome://global/content/bindings/popup.xml",
+      "_openAutocompletePopup@chrome://browser/content/urlbarBindings.xml",
+      "openAutocompletePopup@chrome://browser/content/urlbarBindings.xml",
+      "openPopup@chrome://global/content/bindings/autocomplete.xml",
+      "set_popupOpen@chrome://global/content/bindings/autocomplete.xml",
+    ],
+  },
+];
+
+/* These reflows happen everytime the awesomebar panel opens. */
+const EXPECTED_REFLOWS_SECOND_OPEN = [
+  {
+    stack: [
+      "adjustHeight@chrome://global/content/bindings/autocomplete.xml",
+      "onxblpopupshown@chrome://global/content/bindings/autocomplete.xml"
+    ],
+    times: 3, // This number should only ever go down - never up.
+  },
+
+  {
+    stack: [
+      "adjustHeight@chrome://global/content/bindings/autocomplete.xml",
+      "_invalidate/this._adjustHeightTimeout<@chrome://global/content/bindings/autocomplete.xml",
+    ],
+    times: 3, // This number should only ever go down - never up.
+  },
+
+  {
+    stack: [
+      "_handleOverflow@chrome://global/content/bindings/autocomplete.xml",
+      "handleOverUnderflow@chrome://global/content/bindings/autocomplete.xml",
+      "_reuseAcItem@chrome://global/content/bindings/autocomplete.xml",
+      "_appendCurrentResult@chrome://global/content/bindings/autocomplete.xml",
+      "_invalidate@chrome://global/content/bindings/autocomplete.xml",
+      "invalidate@chrome://global/content/bindings/autocomplete.xml"
+    ],
+    times: 444, // This number should only ever go down - never up.
+  },
+
+  // Bug 1384256
+  {
+    stack: [
+      "_openAutocompletePopup@chrome://browser/content/urlbarBindings.xml",
+      "openAutocompletePopup@chrome://browser/content/urlbarBindings.xml",
+      "openPopup@chrome://global/content/bindings/autocomplete.xml",
+      "set_popupOpen@chrome://global/content/bindings/autocomplete.xml",
+    ],
+    times: 3, // This number should only ever go down - never up.
+  },
+
+  // Bug 1359989
+  {
+    stack: [
+      "openPopup@chrome://global/content/bindings/popup.xml",
+      "_openAutocompletePopup@chrome://browser/content/urlbarBindings.xml",
+      "openAutocompletePopup@chrome://browser/content/urlbarBindings.xml",
+      "openPopup@chrome://global/content/bindings/autocomplete.xml",
+      "set_popupOpen@chrome://global/content/bindings/autocomplete.xml",
+    ],
+  },
+];
+
+/**
+ * Returns a Promise that resolves once the AwesomeBar popup for a particular
+ * window has appeared after having done a search for its input text.
+ *
+ * @param win (browser window)
+ *        The window to do the search in.
+ * @returns Promise
+ */
+async function promiseAutocompleteResultPopup(win) {
+  let URLBar = win.gURLBar;
+  URLBar.controller.startSearch(URLBar.value);
+  await BrowserTestUtils.waitForEvent(URLBar.popup, "popupshown");
+  await BrowserTestUtils.waitForCondition(() => {
+    return URLBar.controller.searchStatus >=
+      Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH;
+  });
+  let matchCount = URLBar.popup._matchCount;
+  await BrowserTestUtils.waitForCondition(() => {
+    return URLBar.popup.richlistbox.childNodes.length == matchCount;
+  });
+
+  URLBar.controller.stopSearch();
+  // There are several setTimeout(fn, 0); calls inside autocomplete.xml
+  // that we need to wait for. Since those have higher priority than
+  // idle callbacks, we can be sure they will have run once this
+  // idle callback is called. The timeout seems to be required in
+  // automation - presumably because the machines can be pretty busy
+  // especially if it's GC'ing from previous tests.
+  await new Promise(resolve => win.requestIdleCallback(resolve, { timeout: 1000 }));
+
+  let hiddenPromise = BrowserTestUtils.waitForEvent(URLBar.popup, "popuphidden");
+  EventUtils.synthesizeKey("VK_ESCAPE", {}, win);
+  await hiddenPromise;
+}
+
+const SEARCH_TERM = "urlbar-reflows";
+
+add_task(async function setup() {
+  const NUM_VISITS = 10;
+  let visits = [];
+
+  for (let i = 0; i < NUM_VISITS; ++i) {
+    visits.push({
+      uri: `http://example.com/urlbar-reflows-${i}`,
+      title: `Reflow test for URL bar entry #${i}`,
+    });
+  }
+
+  await PlacesTestUtils.addVisits(visits);
+
+  registerCleanupFunction(async function() {
+    await PlacesTestUtils.clearHistory();
+  });
+});
+
+/**
+ * This test ensures that there are no unexpected
+ * uninterruptible reflows when typing into the URL bar
+ * with the default values in Places.
+ */
+add_task(async function() {
+  let win = await BrowserTestUtils.openNewBrowserWindow();
+  await ensureNoPreloadedBrowser(win);
+
+  let URLBar = win.gURLBar;
+  let popup = URLBar.popup;
+
+  URLBar.focus();
+  URLBar.value = SEARCH_TERM;
+  let testFn = async function(dirtyFrameFn) {
+    let oldInvalidate = popup.invalidate.bind(popup);
+    let oldResultsAdded = popup.onResultsAdded.bind(popup);
+
+    // We need to invalidate the frame tree outside of the normal
+    // mechanism since invalidations and result additions to the
+    // URL bar occur without firing JS events (which is how we
+    // normally know to dirty the frame tree).
+    popup.invalidate = (reason) => {
+      dirtyFrameFn();
+      oldInvalidate(reason);
+    };
+
+    popup.onResultsAdded = () => {
+      dirtyFrameFn();
+      oldResultsAdded();
+    };
+
+    await promiseAutocompleteResultPopup(win);
+  };
+
+  await withReflowObserver(testFn, EXPECTED_REFLOWS_FIRST_OPEN, win);
+
+  await withReflowObserver(testFn, EXPECTED_REFLOWS_SECOND_OPEN, win);
+
+  await BrowserTestUtils.closeWindow(win);
+});
--- a/browser/base/content/test/performance/browser_windowopen_reflows.js
+++ b/browser/base/content/test/performance/browser_windowopen_reflows.js
@@ -8,124 +8,104 @@
  * is a whitelist that should slowly go away as we improve the performance of
  * the front-end. Instead of adding more reflows to the whitelist, you should
  * be modifying your code to avoid the reflow.
  *
  * See https://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
  * for tips on how to do that.
  */
 const EXPECTED_REFLOWS = [
-  [
-    "select@chrome://global/content/bindings/textbox.xml",
-    "focusAndSelectUrlBar@chrome://browser/content/browser.js",
-    "_delayedStartup@chrome://browser/content/browser.js",
-  ],
+  {
+    stack: [
+      "select@chrome://global/content/bindings/textbox.xml",
+      "focusAndSelectUrlBar@chrome://browser/content/browser.js",
+      "_delayedStartup@chrome://browser/content/browser.js",
+    ],
+  },
 ];
 
 if (Services.appinfo.OS == "Linux") {
   if (gMultiProcessBrowser) {
-    EXPECTED_REFLOWS.push(
-      [
+    EXPECTED_REFLOWS.push({
+      stack: [
         "handleEvent@chrome://browser/content/tabbrowser.xml",
         "EventListener.handleEvent*tabbrowser-tabs_XBL_Constructor@chrome://browser/content/tabbrowser.xml",
       ],
-    );
+    });
   } else {
-    EXPECTED_REFLOWS.push(
-      [
+    EXPECTED_REFLOWS.push({
+      stack: [
         "handleEvent@chrome://browser/content/tabbrowser.xml",
         "inferFromText@chrome://browser/content/browser.js",
         "handleEvent@chrome://browser/content/browser.js",
       ],
-    );
+    });
   }
 }
 
 if (Services.appinfo.OS == "Darwin") {
-  EXPECTED_REFLOWS.push(
-    [
+  EXPECTED_REFLOWS.push({
+    stack: [
       "handleEvent@chrome://browser/content/tabbrowser.xml",
       "inferFromText@chrome://browser/content/browser.js",
       "handleEvent@chrome://browser/content/browser.js",
     ],
-  );
+  });
 }
 
 if (Services.appinfo.OS == "WINNT") {
   EXPECTED_REFLOWS.push(
-    [
-      "verticalMargins@chrome://browser/content/browser-tabsintitlebar.js",
-      "_update@chrome://browser/content/browser-tabsintitlebar.js",
-      "updateAppearance@chrome://browser/content/browser-tabsintitlebar.js",
-      "handleEvent@chrome://browser/content/tabbrowser.xml",
-    ],
+    {
+      stack: [
+        "verticalMargins@chrome://browser/content/browser-tabsintitlebar.js",
+        "_update@chrome://browser/content/browser-tabsintitlebar.js",
+        "updateAppearance@chrome://browser/content/browser-tabsintitlebar.js",
+        "handleEvent@chrome://browser/content/tabbrowser.xml",
+      ],
+      times: 2, // This number should only ever go down - never up.
+    },
 
-    [
-      "verticalMargins@chrome://browser/content/browser-tabsintitlebar.js",
-      "_update@chrome://browser/content/browser-tabsintitlebar.js",
-      "updateAppearance@chrome://browser/content/browser-tabsintitlebar.js",
-      "handleEvent@chrome://browser/content/tabbrowser.xml",
-    ],
+    {
+      stack: [
+        "handleEvent@chrome://browser/content/tabbrowser.xml",
+        "inferFromText@chrome://browser/content/browser.js",
+        "handleEvent@chrome://browser/content/browser.js",
+      ],
+    },
 
-    [
-      "handleEvent@chrome://browser/content/tabbrowser.xml",
-      "inferFromText@chrome://browser/content/browser.js",
-      "handleEvent@chrome://browser/content/browser.js",
-    ],
-
-    [
-      "handleEvent@chrome://browser/content/tabbrowser.xml",
-      "EventListener.handleEvent*tabbrowser-tabs_XBL_Constructor@chrome://browser/content/tabbrowser.xml",
-    ],
+    {
+      stack: [
+        "handleEvent@chrome://browser/content/tabbrowser.xml",
+        "EventListener.handleEvent*tabbrowser-tabs_XBL_Constructor@chrome://browser/content/tabbrowser.xml",
+      ],
+    }
   );
 }
 
 if (Services.appinfo.OS == "WINNT" || Services.appinfo.OS == "Darwin") {
   EXPECTED_REFLOWS.push(
-    [
-      "rect@chrome://browser/content/browser-tabsintitlebar.js",
-      "_update@chrome://browser/content/browser-tabsintitlebar.js",
-      "updateAppearance@chrome://browser/content/browser-tabsintitlebar.js",
-      "handleEvent@chrome://browser/content/tabbrowser.xml",
-    ],
-
-    [
-      "rect@chrome://browser/content/browser-tabsintitlebar.js",
-      "_update@chrome://browser/content/browser-tabsintitlebar.js",
-      "updateAppearance@chrome://browser/content/browser-tabsintitlebar.js",
-      "handleEvent@chrome://browser/content/tabbrowser.xml",
-    ],
-
-    [
-      "rect@chrome://browser/content/browser-tabsintitlebar.js",
-      "_update@chrome://browser/content/browser-tabsintitlebar.js",
-      "updateAppearance@chrome://browser/content/browser-tabsintitlebar.js",
-      "handleEvent@chrome://browser/content/tabbrowser.xml",
-    ],
+    {
+      stack: [
+        "rect@chrome://browser/content/browser-tabsintitlebar.js",
+        "_update@chrome://browser/content/browser-tabsintitlebar.js",
+        "updateAppearance@chrome://browser/content/browser-tabsintitlebar.js",
+        "handleEvent@chrome://browser/content/tabbrowser.xml",
+      ],
+      times: 4, // This number should only ever go down - never up.
+    },
 
-    [
-      "rect@chrome://browser/content/browser-tabsintitlebar.js",
-      "_update@chrome://browser/content/browser-tabsintitlebar.js",
-      "updateAppearance@chrome://browser/content/browser-tabsintitlebar.js",
-      "handleEvent@chrome://browser/content/tabbrowser.xml",
-    ],
-
-    [
-      "verticalMargins@chrome://browser/content/browser-tabsintitlebar.js",
-      "_update@chrome://browser/content/browser-tabsintitlebar.js",
-      "updateAppearance@chrome://browser/content/browser-tabsintitlebar.js",
-      "handleEvent@chrome://browser/content/tabbrowser.xml",
-    ],
-
-    [
-      "verticalMargins@chrome://browser/content/browser-tabsintitlebar.js",
-      "_update@chrome://browser/content/browser-tabsintitlebar.js",
-      "updateAppearance@chrome://browser/content/browser-tabsintitlebar.js",
-      "handleEvent@chrome://browser/content/tabbrowser.xml",
-    ],
+    {
+      stack: [
+        "verticalMargins@chrome://browser/content/browser-tabsintitlebar.js",
+        "_update@chrome://browser/content/browser-tabsintitlebar.js",
+        "updateAppearance@chrome://browser/content/browser-tabsintitlebar.js",
+        "handleEvent@chrome://browser/content/tabbrowser.xml",
+      ],
+      times: 2, // This number should only ever go down - never up.
+    }
   );
 }
 
 /*
  * This test ensures that there are no unexpected
  * uninterruptible reflows when opening new windows.
  */
 add_task(async function() {
--- a/browser/base/content/test/performance/head.js
+++ b/browser/base/content/test/performance/head.js
@@ -1,94 +1,82 @@
 /**
  * Async utility function for ensuring that no unexpected uninterruptible
  * reflows occur during some period of time in a window.
  *
- * The helper works by running a JS function before each event is
- * dispatched that attempts to dirty the layout tree - the idea being
- * that this puts us in the "worst case scenario" so that any JS
- * that attempts to query for layout or style information will cause
- * a reflow to fire. We also dirty the layout tree after each reflow
- * occurs, for good measure.
- *
- * This sounds good in theory, but it's trickier in practice due to
- * various optimizations in our Layout engine. The default function
- * for dirtying the layout tree adds a margin to the first element
- * child it finds in the window to a maximum of 3px, and then goes
- * back to 0px again and loops.
- *
- * This is not sufficient for reflows that we expect to happen within
- * scrollable frames, as Gecko is able to side-step reflowing the
- * contents of a scrollable frame if outer frames are dirtied. Because
- * of this, it's currently possible to override the default node to
- * dirty with one more appropriate for the test.
- *
- * It is also theoretically possible for enough events to fire between
- * reflows such that the before and after state of the layout tree is
- * exactly the same, meaning that no reflow is required, which opens
- * us up to missing expected reflows. This seems to be possible in
- * theory, but hasn't yet shown up in practice - it's just something
- * to be aware of.
- *
- * Bug 1363361 has been filed for a more reliable way of dirtying layout.
- *
  * @param testFn (async function)
  *        The async function that will exercise the browser activity that is
  *        being tested for reflows.
- * @param expectedStacks (Array, optional)
- *        An Array of Arrays representing stacks.
+ *
+ *        The testFn will be passed a single argument, which is a frame dirtying
+ *        function that can be called if the test needs to trigger frame
+ *        dirtying outside of the normal mechanism.
+ * @param expectedReflows (Array, optional)
+ *        An Array of Objects representing reflows.
  *
  *        Example:
  *
  *        [
- *          // This reflow is caused by lorem ipsum
- *          [
- *            "select@chrome://global/content/bindings/textbox.xml",
- *            "focusAndSelectUrlBar@chrome://browser/content/browser.js",
- *            "openLinkIn@chrome://browser/content/utilityOverlay.js",
- *            "openUILinkIn@chrome://browser/content/utilityOverlay.js",
- *            "BrowserOpenTab@chrome://browser/content/browser.js",
- *          ],
+ *          {
+ *            // This reflow is caused by lorem ipsum
+ *            stack: [
+ *              "select@chrome://global/content/bindings/textbox.xml",
+ *              "focusAndSelectUrlBar@chrome://browser/content/browser.js",
+ *              "openLinkIn@chrome://browser/content/utilityOverlay.js",
+ *              "openUILinkIn@chrome://browser/content/utilityOverlay.js",
+ *              "BrowserOpenTab@chrome://browser/content/browser.js",
+ *            ],
+ *            // We expect this particular reflow to happen 2 times
+ *            times: 2,
+ *          },
  *
- *          // This reflow is caused by lorem ipsum
- *          [
- *            "get_scrollPosition@chrome://global/content/bindings/scrollbox.xml",
- *            "_fillTrailingGap@chrome://browser/content/tabbrowser.xml",
- *            "_handleNewTab@chrome://browser/content/tabbrowser.xml",
- *            "onxbltransitionend@chrome://browser/content/tabbrowser.xml",
- *          ],
+ *          {
+ *            // This reflow is caused by lorem ipsum. We expect this reflow
+ *            // to only happen once, so we can omit the "times" property.
+ *            stack: [
+ *              "get_scrollPosition@chrome://global/content/bindings/scrollbox.xml",
+ *              "_fillTrailingGap@chrome://browser/content/tabbrowser.xml",
+ *              "_handleNewTab@chrome://browser/content/tabbrowser.xml",
+ *              "onxbltransitionend@chrome://browser/content/tabbrowser.xml",
+ *            ],
+ *          }
  *
  *        ]
  *
  *        Note that line numbers are not included in the stacks.
  *
  *        Order of the reflows doesn't matter. Expected reflows that aren't seen
  *        will cause an assertion failure. When this argument is not passed,
  *        it defaults to the empty Array, meaning no reflows are expected.
  * @param window (browser window, optional)
  *        The browser window to monitor. Defaults to the current window.
  */
-async function withReflowObserver(testFn, expectedStacks = [], win = window) {
+async function withReflowObserver(testFn, expectedReflows = [], win = window) {
   let dwu = win.QueryInterface(Ci.nsIInterfaceRequestor)
                .getInterface(Ci.nsIDOMWindowUtils);
   let dirtyFrameFn = () => {
     try {
       dwu.ensureDirtyRootFrame();
     } catch (e) {
       // If this fails, we should probably make note of it, but it's not fatal.
       info("Note: ensureDirtyRootFrame threw an exception.");
     }
   };
 
   let els = Cc["@mozilla.org/eventlistenerservice;1"]
               .getService(Ci.nsIEventListenerService);
 
-  // We're going to remove the stacks one by one as we see them so that
+  // We're going to remove the reflows one by one as we see them so that
   // we can check for expected, unseen reflows, so let's clone the array.
-  expectedStacks = expectedStacks.slice(0);
+  // While we're at it, for reflows that omit the "times" property, default
+  // it to 1.
+  expectedReflows = expectedReflows.slice(0);
+  expectedReflows.forEach(r => {
+    r.times = r.times || 1;
+  });
 
   let observer = {
     reflow(start, end) {
       // Gather information about the current code path, slicing out the current
       // frame.
       let path = (new Error().stack).split("\n").slice(1).map(line => {
         return line.replace(/:\d+:\d+$/, "");
       }).join("|");
@@ -99,22 +87,24 @@ async function withReflowObserver(testFn
       dirtyFrameFn();
 
       // Stack trace is empty. Reflow was triggered by native code, which
       // we ignore.
       if (path === "") {
         return;
       }
 
-      let index = expectedStacks.findIndex(stack => path.startsWith(stack.join("|")));
+      let index = expectedReflows.findIndex(reflow => path.startsWith(reflow.stack.join("|")));
 
       if (index != -1) {
         Assert.ok(true, "expected uninterruptible reflow: '" +
                   JSON.stringify(pathWithLineNumbers, null, "\t") + "'");
-        expectedStacks.splice(index, 1);
+        if (--expectedReflows[index].times == 0) {
+          expectedReflows.splice(index, 1);
+        }
       } else {
         Assert.ok(false, "unexpected uninterruptible reflow \n" +
                          JSON.stringify(pathWithLineNumbers, null, "\t") + "\n");
       }
     },
 
     reflowInterruptible(start, end) {
       // We're not interested in interruptible reflows, but might as well take the
@@ -130,26 +120,26 @@ async function withReflowObserver(testFn
                     .getInterface(Ci.nsIWebNavigation)
                     .QueryInterface(Ci.nsIDocShell);
   docShell.addWeakReflowObserver(observer);
 
   els.addListenerForAllEvents(win, dirtyFrameFn, true);
 
   try {
     dirtyFrameFn();
-    await testFn();
+    await testFn(dirtyFrameFn);
   } finally {
-    for (let remainder of expectedStacks) {
+    for (let remainder of expectedReflows) {
       Assert.ok(false,
-                `Unused expected reflow: ${JSON.stringify(remainder, null, "\t")}.\n` +
+                `Unused expected reflow: ${JSON.stringify(remainder.stack, null, "\t")}\n` +
+                `This reflow was supposed to be hit ${remainder.times} more time(s).\n` +
                 "This is probably a good thing - just remove it from the " +
                 "expected list.");
     }
 
-
     els.removeListenerForAllEvents(win, dirtyFrameFn, true);
     docShell.removeWeakReflowObserver(observer);
   }
 }
 
 async function ensureNoPreloadedBrowser() {
   // If we've got a preloaded browser, get rid of it so that it
   // doesn't interfere with the test if it's loading. We have to
--- a/browser/base/content/test/urlbar/browser_page_action_menu.js
+++ b/browser/base/content/test/urlbar/browser_page_action_menu.js
@@ -18,17 +18,17 @@ const mockRemoteClients = [
 add_task(async function bookmark() {
   // Open a unique page.
   let url = "http://example.com/browser_page_action_menu";
   await BrowserTestUtils.withNewTab(url, async () => {
     // Open the panel.
     await promisePageActionPanelOpen();
 
     // The bookmark button should read "Bookmark This Page" and not be starred.
-    let bookmarkButton = document.getElementById("page-action-bookmark-button");
+    let bookmarkButton = document.getElementById("pageAction-panel-bookmark");
     Assert.equal(bookmarkButton.label, "Bookmark This Page");
     Assert.ok(!bookmarkButton.hasAttribute("starred"));
 
     // Click the button.
     let hiddenPromise = promisePageActionPanelHidden();
     EventUtils.synthesizeMouseAtCenter(bookmarkButton, {});
     await hiddenPromise;
 
@@ -83,45 +83,51 @@ add_task(async function bookmark() {
     // Done.
     hiddenPromise = promisePageActionPanelHidden();
     gPageActionPanel.hidePopup();
     await hiddenPromise;
   });
 });
 
 add_task(async function emailLink() {
-  // Replace the email-link entry point to check whether it's called.
-  let originalFn = MailIntegration.sendLinkForBrowser;
-  let fnCalled = false;
-  MailIntegration.sendLinkForBrowser = () => {
-    fnCalled = true;
-  };
-  registerCleanupFunction(() => {
-    MailIntegration.sendLinkForBrowser = originalFn;
-  });
+  // Open an actionable page so that the main page action button appears.  (It
+  // does not appear on about:blank for example.)
+  let url = "http://example.com/";
+  await BrowserTestUtils.withNewTab(url, async () => {
+    // Replace the email-link entry point to check whether it's called.
+    let originalFn = MailIntegration.sendLinkForBrowser;
+    let fnCalled = false;
+    MailIntegration.sendLinkForBrowser = () => {
+      fnCalled = true;
+    };
+    registerCleanupFunction(() => {
+      MailIntegration.sendLinkForBrowser = originalFn;
+    });
 
-  // Open the panel and click Email Link.
-  await promisePageActionPanelOpen();
-  let emailLinkButton =
-    document.getElementById("page-action-email-link-button");
-  let hiddenPromise = promisePageActionPanelHidden();
-  EventUtils.synthesizeMouseAtCenter(emailLinkButton, {});
-  await hiddenPromise;
+    // Open the panel and click Email Link.
+    await promisePageActionPanelOpen();
+    let emailLinkButton =
+      document.getElementById("pageAction-panel-emailLink");
+    let hiddenPromise = promisePageActionPanelHidden();
+    EventUtils.synthesizeMouseAtCenter(emailLinkButton, {});
+    await hiddenPromise;
 
-  Assert.ok(fnCalled);
+    Assert.ok(fnCalled);
+  });
 });
 
 add_task(async function sendToDevice_nonSendable() {
-  // Open a tab that's not sendable.
-  await BrowserTestUtils.withNewTab("about:blank", async () => {
+  // Open a tab that's not sendable -- but that's also actionable so that the
+  // main page action button appears.
+  await BrowserTestUtils.withNewTab("about:home", async () => {
     await promiseSyncReady();
     // Open the panel.  Send to Device should be disabled.
     await promisePageActionPanelOpen();
     let sendToDeviceButton =
-      document.getElementById("page-action-send-to-device-button");
+      document.getElementById("pageAction-panel-sendToDevice");
     Assert.ok(sendToDeviceButton.disabled);
     let hiddenPromise = promisePageActionPanelHidden();
     gPageActionPanel.hidePopup();
     await hiddenPromise;
   });
 });
 
 add_task(async function sendToDevice_syncNotReady_other_states() {
@@ -137,28 +143,28 @@ add_task(async function sendToDevice_syn
     let cleanUp = () => {
       sandbox.restore();
     };
     registerCleanupFunction(cleanUp);
 
     // Open the panel.
     await promisePageActionPanelOpen();
     let sendToDeviceButton =
-      document.getElementById("page-action-send-to-device-button");
+      document.getElementById("pageAction-panel-sendToDevice");
     Assert.ok(!sendToDeviceButton.disabled);
 
     // Click Send to Device.
     let viewPromise = promisePageActionViewShown();
     EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
     let view = await viewPromise;
-    Assert.equal(view.id, "page-action-sendToDeviceView");
+    Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");
 
     let expectedItems = [
       {
-        id: "page-action-sync-not-ready-button",
+        id: "pageAction-panel-sendToDevice-notReady",
         display: "none",
         disabled: true,
       },
       {
         attrs: {
           label: "Account Not Verified",
         },
         disabled: true
@@ -192,54 +198,54 @@ add_task(async function sendToDevice_syn
     sandbox.stub(gSync, "isSendableURI").returns(true);
 
     sandbox.stub(Weave.Service, "sync").callsFake(() => {
       syncReady.get(() => true);
       lastSync.get(() => Date.now());
       sandbox.stub(gSync, "remoteClients").get(() => mockRemoteClients);
     });
 
-    const setupSendToDeviceView = gPageActionButton.setupSendToDeviceView;
-    sandbox.stub(gPageActionButton, "setupSendToDeviceView").callsFake(() => {
+    let onShowingSubview = BrowserPageActions.sendToDevice.onShowingSubview;
+    sandbox.stub(BrowserPageActions.sendToDevice, "onShowingSubview").callsFake((...args) => {
       this.numCall++ || (this.numCall = 1);
-      setupSendToDeviceView.call(gPageActionButton);
+      onShowingSubview.call(BrowserPageActions.sendToDevice, ...args);
       testSendTabToDeviceMenu(this.numCall);
     });
 
     let cleanUp = () => {
       sandbox.restore();
     };
     registerCleanupFunction(cleanUp);
 
     // Open the panel.
     await promisePageActionPanelOpen();
     let sendToDeviceButton =
-      document.getElementById("page-action-send-to-device-button");
+      document.getElementById("pageAction-panel-sendToDevice");
     Assert.ok(!sendToDeviceButton.disabled);
 
     // Click Send to Device.
     let viewPromise = promisePageActionViewShown();
     EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
     let view = await viewPromise;
-    Assert.equal(view.id, "page-action-sendToDeviceView");
+    Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");
 
     function testSendTabToDeviceMenu(numCall) {
       if (numCall == 1) {
         // "Syncing devices" should be shown.
         checkSendToDeviceItems([
           {
-            id: "page-action-sync-not-ready-button",
+            id: "pageAction-panel-sendToDevice-notReady",
             disabled: true,
           },
         ]);
       } else if (numCall == 2) {
         // The devices should be shown in the subview.
         let expectedItems = [
           {
-            id: "page-action-sync-not-ready-button",
+            id: "pageAction-panel-sendToDevice-notReady",
             display: "none",
             disabled: true,
           },
         ];
         for (let client of mockRemoteClients) {
           expectedItems.push({
             attrs: {
               clientId: client.id,
@@ -273,28 +279,28 @@ add_task(async function sendToDevice_syn
 add_task(async function sendToDevice_notSignedIn() {
   // Open a tab that's sendable.
   await BrowserTestUtils.withNewTab("http://example.com/", async () => {
     await promiseSyncReady();
 
     // Open the panel.
     await promisePageActionPanelOpen();
     let sendToDeviceButton =
-      document.getElementById("page-action-send-to-device-button");
+      document.getElementById("pageAction-panel-sendToDevice");
     Assert.ok(!sendToDeviceButton.disabled);
 
     // Click Send to Device.
     let viewPromise = promisePageActionViewShown();
     EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
     let view = await viewPromise;
-    Assert.equal(view.id, "page-action-sendToDeviceView");
+    Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");
 
     let expectedItems = [
       {
-        id: "page-action-sync-not-ready-button",
+        id: "pageAction-panel-sendToDevice-notReady",
         display: "none",
         disabled: true,
       },
       {
         attrs: {
           label: "Not Connected to Sync",
         },
         disabled: true
@@ -329,28 +335,28 @@ add_task(async function sendToDevice_noD
     let cleanUp = () => {
       sandbox.restore();
     };
     registerCleanupFunction(cleanUp);
 
     // Open the panel.
     await promisePageActionPanelOpen();
     let sendToDeviceButton =
-      document.getElementById("page-action-send-to-device-button");
+      document.getElementById("pageAction-panel-sendToDevice");
     Assert.ok(!sendToDeviceButton.disabled);
 
     // Click Send to Device.
     let viewPromise = promisePageActionViewShown();
     EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
     let view = await viewPromise;
-    Assert.equal(view.id, "page-action-sendToDeviceView");
+    Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");
 
     let expectedItems = [
       {
-        id: "page-action-sync-not-ready-button",
+        id: "pageAction-panel-sendToDevice-notReady",
         display: "none",
         disabled: true,
       },
       {
         attrs: {
           label: "No Devices Connected",
         },
         disabled: true
@@ -389,29 +395,29 @@ add_task(async function sendToDevice_dev
     let cleanUp = () => {
       sandbox.restore();
     };
     registerCleanupFunction(cleanUp);
 
     // Open the panel.
     await promisePageActionPanelOpen();
     let sendToDeviceButton =
-      document.getElementById("page-action-send-to-device-button");
+      document.getElementById("pageAction-panel-sendToDevice");
     Assert.ok(!sendToDeviceButton.disabled);
 
     // Click Send to Device.
     let viewPromise = promisePageActionViewShown();
     EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
     let view = await viewPromise;
-    Assert.equal(view.id, "page-action-sendToDeviceView");
+    Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");
 
     // The devices should be shown in the subview.
     let expectedItems = [
       {
-        id: "page-action-sync-not-ready-button",
+        id: "pageAction-panel-sendToDevice-notReady",
         display: "none",
         disabled: true,
       },
     ];
     for (let client of mockRemoteClients) {
       expectedItems.push({
         attrs: {
           clientId: client.id,
@@ -445,17 +451,18 @@ function promiseSyncReady() {
                   .wrappedJSObject;
   return service.whenLoaded().then(() => {
     UIState.isReady();
     return UIState.refresh();
   });
 }
 
 function checkSendToDeviceItems(expectedItems) {
-  let body = document.getElementById("page-action-sendToDeviceView-body");
+  let body =
+    document.getElementById("pageAction-panel-sendToDevice-subview-body");
   Assert.equal(body.childNodes.length, expectedItems.length);
   for (let i = 0; i < expectedItems.length; i++) {
     let expected = expectedItems[i];
     let actual = body.childNodes[i];
     if (!expected) {
       Assert.equal(actual.localName, "toolbarseparator");
       continue;
     }
--- a/browser/base/content/test/urlbar/browser_page_action_menu_clipboard.js
+++ b/browser/base/content/test/urlbar/browser_page_action_menu_clipboard.js
@@ -2,30 +2,35 @@
 
 const mockRemoteClients = [
   { id: "0", name: "foo", type: "mobile" },
   { id: "1", name: "bar", type: "desktop" },
   { id: "2", name: "baz", type: "mobile" },
 ];
 
 add_task(async function copyURL() {
-  // Open the panel.
-  await promisePageActionPanelOpen();
+  // Open an actionable page so that the main page action button appears.  (It
+  // does not appear on about:blank for example.)
+  let url = "http://example.com/";
+  await BrowserTestUtils.withNewTab(url, async () => {
+    // Open the panel.
+    await promisePageActionPanelOpen();
 
-  // Click Copy URL.
-  let copyURLButton = document.getElementById("page-action-copy-url-button");
-  let hiddenPromise = promisePageActionPanelHidden();
-  EventUtils.synthesizeMouseAtCenter(copyURLButton, {});
-  await hiddenPromise;
+    // Click Copy URL.
+    let copyURLButton = document.getElementById("pageAction-panel-copyURL");
+    let hiddenPromise = promisePageActionPanelHidden();
+    EventUtils.synthesizeMouseAtCenter(copyURLButton, {});
+    await hiddenPromise;
 
-  // Check the clipboard.
-  let transferable = Cc["@mozilla.org/widget/transferable;1"]
-                       .createInstance(Ci.nsITransferable);
-  transferable.init(null);
-  let flavor = "text/unicode";
-  transferable.addDataFlavor(flavor);
-  Services.clipboard.getData(transferable, Services.clipboard.kGlobalClipboard);
-  let strObj = {};
-  transferable.getTransferData(flavor, strObj, {});
-  Assert.ok(!!strObj.value);
-  strObj.value.QueryInterface(Ci.nsISupportsString);
-  Assert.equal(strObj.value.data, gBrowser.selectedBrowser.currentURI.spec);
+    // Check the clipboard.
+    let transferable = Cc["@mozilla.org/widget/transferable;1"]
+                         .createInstance(Ci.nsITransferable);
+    transferable.init(null);
+    let flavor = "text/unicode";
+    transferable.addDataFlavor(flavor);
+    Services.clipboard.getData(transferable, Services.clipboard.kGlobalClipboard);
+    let strObj = {};
+    transferable.getTransferData(flavor, strObj, {});
+    Assert.ok(!!strObj.value);
+    strObj.value.QueryInterface(Ci.nsISupportsString);
+    Assert.equal(strObj.value.data, gBrowser.selectedBrowser.currentURI.spec);
+  });
 });
--- a/browser/base/content/test/urlbar/head.js
+++ b/browser/base/content/test/urlbar/head.js
@@ -195,20 +195,20 @@ function promiseNewSearchEngine(basename
       onError(errCode) {
         Assert.ok(false, "addEngine failed with error code " + errCode);
         reject();
       },
     });
   });
 }
 
-let gPageActionPanel = document.getElementById("page-action-panel");
+let gPageActionPanel = document.getElementById("pageActionPanel");
 
 function promisePageActionPanelOpen() {
-  let button = document.getElementById("urlbar-page-action-button");
+  let button = document.getElementById("pageActionButton");
   let shownPromise = promisePageActionPanelShown();
   EventUtils.synthesizeMouseAtCenter(button, {});
   return shownPromise;
 }
 
 function promisePageActionPanelShown() {
   return promisePageActionPanelEvent("popupshown");
 }
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -69,16 +69,17 @@ browser.jar:
         content/browser/browser-customization.js      (content/browser-customization.js)
         content/browser/browser-data-submission-info-bar.js (content/browser-data-submission-info-bar.js)
         content/browser/browser-compacttheme.js       (content/browser-compacttheme.js)
         content/browser/browser-feeds.js              (content/browser-feeds.js)
         content/browser/browser-fullScreenAndPointerLock.js  (content/browser-fullScreenAndPointerLock.js)
         content/browser/browser-fullZoom.js           (content/browser-fullZoom.js)
         content/browser/browser-gestureSupport.js     (content/browser-gestureSupport.js)
         content/browser/browser-media.js              (content/browser-media.js)
+        content/browser/browser-pageActions.js        (content/browser-pageActions.js)
         content/browser/browser-places.js             (content/browser-places.js)
         content/browser/browser-plugins.js            (content/browser-plugins.js)
         content/browser/browser-safebrowsing.js       (content/browser-safebrowsing.js)
         content/browser/browser-sidebar.js            (content/browser-sidebar.js)
         content/browser/browser-social.js             (content/browser-social.js)
         content/browser/browser-sync.js               (content/browser-sync.js)
 *       content/browser/browser-tabPreviews.xml       (content/browser-tabPreviews.xml)
 #ifdef CAN_DRAW_IN_TITLEBAR
--- a/browser/components/customizableui/PanelMultiView.jsm
+++ b/browser/components/customizableui/PanelMultiView.jsm
@@ -287,17 +287,18 @@ this.PanelMultiView = class {
         this.setMainView(this._mainView);
       }
     }
 
     this.node.setAttribute("viewtype", "main");
 
     // Proxy these public properties and methods, as used elsewhere by various
     // parts of the browser, to this instance.
-    ["_mainView", "ignoreMutations", "showingSubView"].forEach(property => {
+    ["_mainView", "ignoreMutations", "showingSubView",
+     "_panelViews"].forEach(property => {
       Object.defineProperty(this.node, property, {
         enumerable: true,
         get: () => this[property],
         set: (val) => this[property] = val
       });
     });
     ["goBack", "descriptionHeightWorkaround", "setMainView", "showMainView",
      "showSubView"].forEach(method => {
--- a/browser/components/extensions/ext-url-overrides.js
+++ b/browser/components/extensions/ext-url-overrides.js
@@ -22,16 +22,18 @@ this.urlOverrides = class extends Extens
       aboutNewTabService.newTabURL = item.value || item.initialValue;
     }
   }
 
   async onManifestEntry(entryName) {
     let {extension} = this;
     let {manifest} = extension;
 
+    await ExtensionSettingsStore.initialize();
+
     if (manifest.chrome_url_overrides.newtab) {
       // Set up the shutdown code for the setting.
       extension.callOnClose({
         close: () => {
           switch (extension.shutdownReason) {
             case "ADDON_DISABLE":
               this.processNewTabSetting("disable");
               break;
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -29,16 +29,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
           AsyncPrefs: false, AsyncShutdown:false, AutoCompletePopup:false, BookmarkHTMLUtils:false,
           BookmarkJSONUtils:false, BrowserUITelemetry:false, BrowserUsageTelemetry:false,
           ContentClick:false, ContentPrefServiceParent:false, ContentSearch:false,
           DateTimePickerHelper:false, DirectoryLinksProvider:false,
           ExtensionsUI:false, Feeds:false,
           FileUtils:false, FormValidationHandler:false, Integration:false,
           LightweightThemeManager:false, LoginHelper:false, LoginManagerParent:false,
           NetUtil:false, NewTabUtils:false, OS:false,
+          PageActions:false,
           PageThumbs:false, PdfJs:false, PermissionUI:false, PlacesBackups:false,
           PlacesUtils:false, PluralForm:false, PrivateBrowsingUtils:false,
           ProcessHangMonitor:false, ReaderParent:false, RecentWindow:false,
           RemotePrompt:false, SessionStore:false,
           ShellService:false, SimpleServiceDiscovery:false, TabCrashHandler:false,
           UITour:false, UIState:false, UpdateListener:false, WebChannel:false,
           WindowsRegistry:false, webrtcUI:false */
 
@@ -74,16 +75,17 @@ let initializedModules = {};
   ["FormValidationHandler", "resource:///modules/FormValidationHandler.jsm"],
   ["Integration", "resource://gre/modules/Integration.jsm"],
   ["LightweightThemeManager", "resource://gre/modules/LightweightThemeManager.jsm"],
   ["LoginHelper", "resource://gre/modules/LoginHelper.jsm"],
   ["LoginManagerParent", "resource://gre/modules/LoginManagerParent.jsm"],
   ["NetUtil", "resource://gre/modules/NetUtil.jsm"],
   ["NewTabUtils", "resource://gre/modules/NewTabUtils.jsm"],
   ["OS", "resource://gre/modules/osfile.jsm"],
+  ["PageActions", "resource:///modules/PageActions.jsm"],
   ["PageThumbs", "resource://gre/modules/PageThumbs.jsm"],
   ["PdfJs", "resource://pdf.js/PdfJs.jsm"],
   ["PermissionUI", "resource:///modules/PermissionUI.jsm"],
   ["PlacesBackups", "resource://gre/modules/PlacesBackups.jsm"],
   ["PlacesUtils", "resource://gre/modules/PlacesUtils.jsm"],
   ["PluralForm", "resource://gre/modules/PluralForm.jsm"],
   ["PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"],
   ["ProcessHangMonitor", "resource:///modules/ProcessHangMonitor.jsm"],
@@ -967,16 +969,18 @@ BrowserGlue.prototype = {
 
     PageThumbs.init();
 
     DirectoryLinksProvider.init();
     NewTabUtils.init();
     NewTabUtils.links.addProvider(DirectoryLinksProvider);
     AboutNewTab.init();
 
+    PageActions.init();
+
     this._firstWindowTelemetry(aWindow);
     this._firstWindowLoaded();
 
     this._mediaTelemetryIdleObserver = {
       browserGlue: this,
       observe(aSubject, aTopic, aData) {
         if (aTopic != "idle") {
           return;
--- a/browser/components/preferences/in-content-new/findInPage.js
+++ b/browser/components/preferences/in-content-new/findInPage.js
@@ -14,31 +14,32 @@ var gSearchResultsPane = {
     if (this.inited) {
       return;
     }
     this.inited = true;
     this.searchInput = document.getElementById("searchInput");
     this.searchInput.hidden = !Services.prefs.getBoolPref("browser.preferences.search");
     if (!this.searchInput.hidden) {
       this.searchInput.addEventListener("command", this);
-      window.addEventListener("load", () => {
+      window.addEventListener("DOMContentLoaded", () => {
         this.searchInput.focus();
-        this.initializeCategories();
       });
+      // Initialize other panes in an idle callback.
+      window.requestIdleCallback(() => this.initializeCategories());
     }
     let strings = this.strings;
     this.searchInput.placeholder = AppConstants.platform == "win" ?
       strings.getString("searchInput.labelWin") :
       strings.getString("searchInput.labelUnix");
   },
 
   handleEvent(event) {
-    if (event.type === "command") {
-      this.searchFunction(event);
-    }
+    // Ensure categories are initialized if idle callback didn't run sooo enough.
+    this.initializeCategories();
+    this.searchFunction(event);
   },
 
   /**
    * Check that the passed string matches the filter arguments.
    *
    * @param String str
    *    to search for filter words in.
    * @param String filter
--- a/browser/components/sessionstore/test/browser.ini
+++ b/browser/components/sessionstore/test/browser.ini
@@ -249,15 +249,15 @@ skip-if = debug
 [browser_docshell_uuid_consistency.js]
 [browser_grouped_session_store.js]
 skip-if = !e10s # GroupedSHistory is e10s-only
 
 [browser_closed_objects_changed_notifications_tabs.js]
 [browser_closed_objects_changed_notifications_windows.js]
 [browser_duplicate_history.js]
 [browser_tabicon_after_bg_tab_crash.js]
-skip-if = !e10s # Tabs can't crash without e10s
+skip-if = !crashreporter || !e10s # Tabs can't crash without e10s
 
 [browser_cookies.js]
 [browser_cookies_legacy.js]
 [browser_cookies_privacy.js]
 [browser_speculative_connect.js]
 
--- a/browser/extensions/formautofill/FormAutofillContent.jsm
+++ b/browser/extensions/formautofill/FormAutofillContent.jsm
@@ -90,17 +90,17 @@ AutofillProfileAutoCompleteSearch.protot
    */
   startSearch(searchString, searchParam, previousResult, listener) {
     this.log.debug("startSearch: for", searchString, "with input", formFillController.focusedInput);
     let focusedInput = formFillController.focusedInput;
     this.forceStop = false;
     let info = FormAutofillContent.getInputDetails(focusedInput);
 
     if (!FormAutofillContent.savedFieldNames.has(info.fieldName) ||
-        FormAutofillContent.getFormHandler(focusedInput).filledProfileGUID) {
+        FormAutofillContent.getFormHandler(focusedInput).address.filledRecordGUID) {
       let formHistory = Cc["@mozilla.org/autocomplete/search;1?name=form-history"]
                           .createInstance(Ci.nsIAutoCompleteSearch);
       formHistory.startSearch(searchString, searchParam, previousResult, {
         onSearchResult: (search, result) => {
           listener.onSearchResult(this, result);
           ProfileAutocomplete.setProfileAutoCompleteResult(result);
         },
       });
@@ -372,29 +372,38 @@ var FormAutofillContent = {
     }
 
     let handler = this._formsDetails.get(formElement);
     if (!handler) {
       this.log.debug("Form element could not map to an existing handler");
       return true;
     }
 
-    let pendingAddress = handler.createProfile();
-    if (Object.keys(pendingAddress).length < FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD) {
-      this.log.debug(`Not saving since there are only ${Object.keys(pendingAddress).length} usable fields`);
+    let {addressRecord, creditCardRecord} = handler.createRecords();
+
+    if (!addressRecord && !creditCardRecord) {
       return true;
     }
 
-    this._onFormSubmit({
-      address: {
-        guid: handler.filledProfileGUID,
-        record: pendingAddress,
-      },
-      // creditCard: {}
-    }, domWin);
+    let data = {};
+    if (addressRecord) {
+      data.address = {
+        guid: handler.address.filledRecordGUID,
+        record: addressRecord,
+      };
+    }
+
+    if (creditCardRecord) {
+      data.creditCard = {
+        guid: handler.creditCard.filledRecordGUID,
+        record: creditCardRecord,
+      };
+    }
+
+    this._onFormSubmit(data, domWin);
 
     return true;
   },
 
   receiveMessage({name, data}) {
     switch (name) {
       case "FormAutofill:enabledStatus": {
         if (data) {
@@ -479,38 +488,24 @@ var FormAutofillContent = {
     if (!formHandler) {
       let formLike = FormLikeFactory.createFromField(element);
       formHandler = new FormAutofillHandler(formLike);
     } else if (!formHandler.isFormChangedSinceLastCollection) {
       this.log.debug("No control is removed or inserted since last collection.");
       return;
     }
 
-    formHandler.collectFormFields();
+    let validDetails = formHandler.collectFormFields();
 
     this._formsDetails.set(formHandler.form.rootElement, formHandler);
     this.log.debug("Adding form handler to _formsDetails:", formHandler);
 
-    if (formHandler.isValidAddressForm) {
-      formHandler.addressFieldDetails.forEach(
-        detail => this._markAsAutofillField(detail.elementWeakRef.get())
-      );
-    } else {
-      this.log.debug("Ignoring address related fields since it has only",
-                     formHandler.addressFieldDetails.length,
-                     "field(s)");
-    }
-
-    if (formHandler.isValidCreditCardForm) {
-      formHandler.creditCardFieldDetails.forEach(
-        detail => this._markAsAutofillField(detail.elementWeakRef.get())
-      );
-    } else {
-      this.log.debug("Ignoring credit card related fields since it's without credit card number field");
-    }
+    validDetails.forEach(detail =>
+      this._markAsAutofillField(detail.elementWeakRef.get())
+    );
   },
 
   _markAsAutofillField(field) {
     // Since Form Autofill popup is only for input element, any non-Input
     // element should be excluded here.
     if (!field || !(field instanceof Ci.nsIDOMHTMLInputElement)) {
       return;
     }
--- a/browser/extensions/formautofill/FormAutofillHandler.jsm
+++ b/browser/extensions/formautofill/FormAutofillHandler.jsm
@@ -26,16 +26,38 @@ FormAutofillUtils.defineLazyLogGetter(th
  * Handles profile autofill for a DOM Form element.
  * @param {FormLike} form Form that need to be auto filled
  */
 function FormAutofillHandler(form) {
   this.form = form;
   this.fieldDetails = [];
   this.winUtils = this.form.rootElement.ownerGlobal.QueryInterface(Ci.nsIInterfaceRequestor)
     .getInterface(Ci.nsIDOMWindowUtils);
+
+  this.address = {
+    /**
+     * Similar to the `fieldDetails` above but contains address fields only.
+     */
+    fieldDetails: [],
+    /**
+     * String of the filled address' guid.
+     */
+    filledRecordGUID: null,
+  };
+
+  this.creditCard = {
+    /**
+     * Similar to the `fieldDetails` above but contains credit card fields only.
+     */
+    fieldDetails: [],
+    /**
+     * String of the filled creditCard's guid.
+     */
+    filledRecordGUID: null,
+  };
 }
 
 FormAutofillHandler.prototype = {
   /**
    * DOM Form element to which this object is attached.
    */
   form: null,
 
@@ -52,39 +74,24 @@ FormAutofillHandler.prototype = {
    * the same exact combination of these values.
    *
    * A direct reference to the associated element cannot be sent to the user
    * interface because processing may be done in the parent process.
    */
   fieldDetails: null,
 
   /**
-   * Similiar to `fieldDetails`, and `addressFieldDetails` contains the address
-   * records only.
+   * Subcategory of handler that contains address related data.
    */
-  addressFieldDetails: null,
+  address: null,
 
   /**
-   * Similiar to `fieldDetails`, and `creditCardFieldDetails` contains the
-   * Credit Card records only.
+   * Subcategory of handler that contains credit card related data.
    */
-  creditCardFieldDetails: null,
-
-  get isValidAddressForm() {
-    return this.addressFieldDetails.length >= FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD;
-  },
-
-  get isValidCreditCardForm() {
-    return this.creditCardFieldDetails.some(i => i.fieldName == "cc-number");
-  },
-
-  /**
-   * String of the filled profile's guid.
-   */
-  filledProfileGUID: null,
+  creditCard: null,
 
   /**
    * A WindowUtils reference of which Window the form belongs
    */
   winUtils: null,
 
   /**
    * Enum for form autofill MANUALLY_MANAGED_STATES values
@@ -103,30 +110,47 @@ FormAutofillHandler.prototype = {
     // can be recognized as there is no element changed. However, we should
     // improve the function to detect the element changes. e.g. a tel field
     // is changed from type="hidden" to type="tel".
     return this._formFieldCount != this.form.elements.length;
   },
 
   /**
    * Set fieldDetails from the form about fields that can be autofilled.
+
+   * @returns {Array} The valid address and credit card details.
    */
   collectFormFields() {
     this._cacheValue.allFieldNames = null;
     this._formFieldCount = this.form.elements.length;
     let fieldDetails = FormAutofillHeuristics.getFormInfo(this.form);
     this.fieldDetails = fieldDetails ? fieldDetails : [];
     log.debug("Collected details on", this.fieldDetails.length, "fields");
 
-    this.addressFieldDetails = this.fieldDetails.filter(
+    this.address.fieldDetails = this.fieldDetails.filter(
       detail => FormAutofillUtils.isAddressField(detail.fieldName)
     );
-    this.creditCardFieldDetails = this.fieldDetails.filter(
+    this.creditCard.fieldDetails = this.fieldDetails.filter(
       detail => FormAutofillUtils.isCreditCardField(detail.fieldName)
     );
+
+    if (this.address.fieldDetails.length < FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD) {
+      log.debug("Ignoring address related fields since it has only",
+                this.address.fieldDetails.length,
+                "field(s)");
+      this.address.fieldDetails = [];
+    }
+
+    if (!this.creditCard.fieldDetails.some(i => i.fieldName == "cc-number")) {
+      log.debug("Ignoring credit card related fields since it's without credit card number field");
+      this.creditCard.fieldDetails = [];
+    }
+
+    return Array.of(...(this.address.fieldDetails),
+                    ...(this.creditCard.fieldDetails));
   },
 
   getFieldDetailByName(fieldName) {
     return this.fieldDetails.find(detail => detail.fieldName == fieldName);
   },
 
   _cacheValue: {
     allFieldNames: null,
@@ -188,18 +212,18 @@ FormAutofillHandler.prototype = {
    * @param {Object} profile
    *        A profile to be filled in.
    * @param {Object} focusedInput
    *        A focused input element which is skipped for filling.
    */
   autofillFormFields(profile, focusedInput) {
     log.debug("profile in autofillFormFields:", profile);
 
-    this.filledProfileGUID = profile.guid;
-    for (let fieldDetail of this.addressFieldDetails) {
+    this.address.filledRecordGUID = profile.guid;
+    for (let fieldDetail of this.address.fieldDetails) {
       // Avoid filling field value in the following cases:
       // 1. the focused input which is filled in FormFillController.
       // 2. a non-empty input field
       // 3. the invalid value set
       // 4. value already chosen in select element
 
       let element = fieldDetail.elementWeakRef.get();
       if (!element) {
@@ -250,17 +274,17 @@ FormAutofillHandler.prototype = {
     log.debug("register change handler for filled form:", this.form);
     const onChangeHandler = e => {
       let hasFilledFields;
 
       if (!e.isTrusted) {
         return;
       }
 
-      for (let fieldDetail of this.addressFieldDetails) {
+      for (let fieldDetail of this.address.fieldDetails) {
         let element = fieldDetail.elementWeakRef.get();
 
         if (!element) {
           return;
         }
 
         if (e.target == element || (e.target == element.form && e.type == "reset")) {
           this.changeFieldState(fieldDetail, "NORMAL");
@@ -268,34 +292,34 @@ FormAutofillHandler.prototype = {
 
         hasFilledFields |= (fieldDetail.state == "AUTO_FILLED");
       }
 
       // Unregister listeners and clear guid once no field is in AUTO_FILLED state.
       if (!hasFilledFields) {
         this.form.rootElement.removeEventListener("input", onChangeHandler);
         this.form.rootElement.removeEventListener("reset", onChangeHandler);
-        this.filledProfileGUID = null;
+        this.address.filledRecordGUID = null;
       }
     };
 
     this.form.rootElement.addEventListener("input", onChangeHandler);
     this.form.rootElement.addEventListener("reset", onChangeHandler);
   },
 
   /**
    * Populates result to the preview layers with given profile.
    *
    * @param {Object} profile
    *        A profile to be previewed with
    */
   previewFormFields(profile) {
     log.debug("preview profile in autofillFormFields:", profile);
 
-    for (let fieldDetail of this.addressFieldDetails) {
+    for (let fieldDetail of this.address.fieldDetails) {
       let element = fieldDetail.elementWeakRef.get();
       let value = profile[fieldDetail.fieldName] || "";
 
       // Skip the field that is null
       if (!element) {
         continue;
       }
 
@@ -317,17 +341,17 @@ FormAutofillHandler.prototype = {
   },
 
   /**
    * Clear preview text and background highlight of all fields.
    */
   clearPreviewedFormFields() {
     log.debug("clear previewed fields in:", this.form);
 
-    for (let fieldDetail of this.addressFieldDetails) {
+    for (let fieldDetail of this.address.fieldDetails) {
       let element = fieldDetail.elementWeakRef.get();
       if (!element) {
         log.warn(fieldDetail.fieldName, "is unreachable");
         continue;
       }
 
       element.previewValue = "";
 
@@ -374,30 +398,52 @@ FormAutofillHandler.prototype = {
         this.winUtils.removeManuallyManagedState(element, mmStateValue);
       }
     }
 
     fieldDetail.state = nextState;
   },
 
   /**
-   * Return the profile that is converted from fieldDetails and only non-empty fields
-   * are included.
+   * Return the records that is converted from address/creditCard fieldDetails and
+   * only valid form records are included.
    *
    * @returns {Object} The new profile that convert from details with trimmed result.
    */
-  createProfile() {
-    let profile = {};
+  createRecords() {
+    let records = {};
 
-    this.addressFieldDetails.forEach(detail => {
-      let element = detail.elementWeakRef.get();
-      // Remove the unnecessary spaces
-      let value = element && element.value.trim();
-      if (!value) {
+    ["address", "creditCard"].forEach(type => {
+      let details = this[type].fieldDetails;
+      if (!details || details.length == 0) {
         return;
       }
 
-      profile[detail.fieldName] = value;
+      let recordName = `${type}Record`;
+      records[recordName] = {};
+      details.forEach(detail => {
+        let element = detail.elementWeakRef.get();
+        // Remove the unnecessary spaces
+        let value = element && element.value.trim();
+        if (!value) {
+          return;
+        }
+
+        records[recordName][detail.fieldName] = value;
+      });
     });
 
-    return profile;
+    if (records.addressRecord &&
+        Object.keys(records.addressRecord).length < FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD) {
+      log.debug("No address record saving since there are only",
+                     Object.keys(records.addressRecord).length,
+                     "usable fields");
+      delete records.addressRecord;
+    }
+
+    if (records.creditCardRecord && !records.creditCardRecord["cc-number"]) {
+      log.debug("No credit card record saving since card number is empty");
+      delete records.creditCardRecord;
+    }
+
+    return records;
   },
 };
--- a/browser/extensions/formautofill/ProfileAutoCompleteResult.jsm
+++ b/browser/extensions/formautofill/ProfileAutoCompleteResult.jsm
@@ -175,16 +175,25 @@ class AddressResult extends ProfileAutoC
         "address-line1",
         "address-line2",
         "address-line3",
       ],
       "country-name": [
         "country",
         "country-name",
       ],
+      "tel": [
+        "tel",
+        "tel-country-code",
+        "tel-national",
+        "tel-area-code",
+        "tel-local",
+        "tel-local-prefix",
+        "tel-local-suffix",
+      ],
     };
 
     const secondaryLabelOrder = [
       "street-address",  // Street address
       "name",            // Full name
       "address-level2",  // City/Town
       "organization",    // Company or organization name
       "address-level1",  // Province/State (Standardized code if possible)
--- a/browser/extensions/formautofill/content/editAddress.xhtml
+++ b/browser/extensions/formautofill/content/editAddress.xhtml
@@ -46,16 +46,17 @@
     </label>
     <label id="country-container">
       <span data-localization="country"/>
       <select id="country">
         <option/>
         <option value="US" data-localization="us"/>
       </select>
     </label>
+    <p id="country-warning-message" data-localization="countryWarningMessage"/>
     <label id="email-container">
       <span data-localization="email"/>
       <input id="email" type="email"/>
     </label>
     <label id="tel-container">
       <span data-localization="tel"/>
       <input id="tel" type="tel"/>
     </label>
--- a/browser/extensions/formautofill/content/manageAddresses.xhtml
+++ b/browser/extensions/formautofill/content/manageAddresses.xhtml
@@ -6,21 +6,16 @@
 <html xmlns="http://www.w3.org/1999/xhtml">
 <head>
   <title data-localization="manageDialogTitle"/>
   <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" />
   <link rel="stylesheet" href="chrome://formautofill/content/manageAddresses.css" />
   <script src="chrome://formautofill/content/manageAddresses.js"></script>
 </head>
 <body>
-  <p style="padding-left: 30px; background: url(chrome://browser/skin/warning.svg) no-repeat left center">
-    Autofill of addresses is only ready for testing with United States addresses on &lt;input&gt;s and some &lt;select&gt; elements.
-    Improvements to form field type detection are in progress.
-    <a href="https://luke-chang.github.io/autofill-demo/basic.html" target="_blank">Demo page</a>
-  </p>
   <fieldset>
     <legend data-localization="addressListHeader"/>
     <select id="addresses" size="9" multiple="multiple"/>
   </fieldset>
   <div id="controls-container">
     <button id="remove" disabled="disabled" data-localization="remove"/>
     <button id="add" data-localization="add"/>
     <button id="edit" disabled="disabled" data-localization="edit"/>
--- a/browser/extensions/formautofill/locales/en-US/formautofill.properties
+++ b/browser/extensions/formautofill/locales/en-US/formautofill.properties
@@ -50,8 +50,9 @@ province = Province
 state = State
 postalCode = Postal Code
 zip = Zip Code
 country = Country or Region
 tel = Phone
 email = Email
 cancel = Cancel
 save = Save
+countryWarningMessage = Autofill is currently available only for US addresses
--- a/browser/extensions/formautofill/skin/shared/editAddress.css
+++ b/browser/extensions/formautofill/skin/shared/editAddress.css
@@ -7,25 +7,27 @@ html {
 }
 
 body {
   font-size: 1rem;
 }
 
 form,
 label,
-div {
+div,
+p {
   display: flex;
 }
 
 form {
   flex-wrap: wrap;
 }
 
-label {
+label,
+p {
   margin: 0 0 0.5em;
 }
 
 label > span {
   box-sizing: border-box;
   flex: 0 0 9.5em;
   padding-inline-end: 0.5em;
   align-self: center;
@@ -44,17 +46,20 @@ option {
   padding: 5px 10px;
 }
 
 textarea {
   resize: none;
 }
 
 button {
+  font-size: 1.2em;
   padding: 3px 2em;
+  margin-inline-start: 10px;
+  margin-inline-end: 0;
 }
 
 #given-name-container,
 #additional-name-container,
 #address-level1-container,
 #postal-code-container,
 #country-container {
   flex: 0 1 50%;
@@ -72,16 +77,24 @@ button {
 
 #controls-container {
   justify-content: end;
 }
 
 #family-name,
 #organization,
 #address-level2,
-#tel{
+#tel {
   flex: 0 0 auto;
 }
 
 #street-address,
 #email {
   flex: 1 0 auto;
 }
+
+#country-warning-message {
+  flex: 1;
+  align-items: center;
+  text-align: start;
+  color: #737373;
+  padding-inline-start: 1em;
+}
--- a/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
+++ b/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
@@ -318,31 +318,31 @@ function do_test(testcases, testFn) {
 
         let doc = MockDocument.createTestDocument("http://localhost:8080/test/",
                                                   testcase.document);
         let form = doc.querySelector("form");
         let formLike = FormLikeFactory.createFromForm(form);
         let handler = new FormAutofillHandler(formLike);
         let promises = [];
 
-        handler.addressFieldDetails = testcase.addressFieldDetails;
-        handler.addressFieldDetails.forEach((field, index) => {
+        handler.address.fieldDetails = testcase.addressFieldDetails;
+        handler.address.fieldDetails.forEach((field, index) => {
           let element = doc.querySelectorAll("input, select")[index];
           field.elementWeakRef = Cu.getWeakReference(element);
           if (!testcase.profileData[field.fieldName]) {
             // Avoid waiting for `change` event of a input with a blank value to
             // be filled.
             return;
           }
           promises.push(...testFn(testcase, element));
         });
 
         handler.autofillFormFields(testcase.profileData);
-        Assert.equal(handler.filledProfileGUID, testcase.profileData.guid,
-                     "Check if filledProfileGUID is set correctly");
+        Assert.equal(handler.address.filledRecordGUID, testcase.profileData.guid,
+                     "Check if filledRecordGUID is set correctly");
         await Promise.all(promises);
       });
     })();
   }
 }
 
 do_test(TESTCASES, (testcase, element) => {
   let id = element.id;
--- a/browser/extensions/formautofill/test/unit/test_collectFormFields.js
+++ b/browser/extensions/formautofill/test/unit/test_collectFormFields.js
@@ -17,20 +17,25 @@ const TESTCASES = [
       {"section": "", "addressType": "", "contactType": "", "fieldName": "family-name"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "address-line1"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "country"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "email"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "tel"},
     ],
     creditCardFieldDetails: [],
-    isValidForm: {
-      address: true,
-      creditCard: false,
-    },
+    validFieldDetails: [
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "given-name"},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "family-name"},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "address-line1"},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2"},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "country"},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "email"},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "tel"},
+    ],
     ids: ["given-name", "family-name", "street-addr", "city", "country", "email", "phone"],
   },
   {
     description: "An address and credit card form with autocomplete properties and 1 token",
     document: `<form>
                <input id="given-name" autocomplete="given-name">
                <input id="family-name" autocomplete="family-name">
                <input id="street-addr" autocomplete="street-address">
@@ -53,20 +58,29 @@ const TESTCASES = [
       {"section": "", "addressType": "", "contactType": "", "fieldName": "tel"},
     ],
     creditCardFieldDetails: [
       {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-number"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-name"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-exp-month"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-exp-year"},
     ],
-    isValidForm: {
-      address: true,
-      creditCard: true,
-    },
+    validFieldDetails: [
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "given-name"},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "family-name"},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "street-address"},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2"},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "country"},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "email"},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "tel"},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-number"},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-name"},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-exp-month"},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-exp-year"},
+    ],
   },
   {
     description: "An address form with autocomplete properties and 2 tokens",
     document: `<form><input id="given-name" autocomplete="shipping given-name">
                <input id="family-name" autocomplete="shipping family-name">
                <input id="street-addr" autocomplete="shipping street-address">
                <input id="city" autocomplete="shipping address-level2">
                <input id="country" autocomplete="shipping country">
@@ -77,20 +91,25 @@ const TESTCASES = [
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel"},
     ],
     creditCardFieldDetails: [],
-    isValidForm: {
-      address: true,
-      creditCard: false,
-    },
+    validFieldDetails: [
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name"},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name"},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address"},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2"},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country"},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email"},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel"},
+    ],
   },
   {
     description: "Form with autocomplete properties and profile is partly matched",
     document: `<form><input id="given-name" autocomplete="shipping given-name">
                <input id="family-name" autocomplete="shipping family-name">
                <input id="street-addr" autocomplete="shipping street-address">
                <input autocomplete="shipping address-level2">
                <select autocomplete="shipping country"></select>
@@ -101,20 +120,25 @@ const TESTCASES = [
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel"},
     ],
     creditCardFieldDetails: [],
-    isValidForm: {
-      address: true,
-      creditCard: false,
-    },
+    validFieldDetails: [
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name"},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name"},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address"},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2"},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country"},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email"},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel"},
+    ],
   },
   {
     description: "It's a valid address and credit card form.",
     document: `<form>
                <input id="given-name" autocomplete="shipping given-name">
                <input id="family-name" autocomplete="shipping family-name">
                <input id="street-addr" autocomplete="shipping street-address">
                <input id="cc-number" autocomplete="shipping cc-number">
@@ -122,43 +146,35 @@ const TESTCASES = [
     addressFieldDetails: [
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address"},
     ],
     creditCardFieldDetails: [
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "cc-number"},
     ],
-    isValidForm: {
-      address: true,
-      creditCard: true,
-    },
+    validFieldDetails: [
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name"},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name"},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address"},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "cc-number"},
+    ],
   },
   {
     description: "It's an invalid address and credit form.",
     document: `<form>
                <input id="given-name" autocomplete="shipping given-name">
                <input autocomplete="shipping address-level2">
                <input id="cc-name" autocomplete="cc-name">
                <input id="cc-exp-month" autocomplete="cc-exp-month">
                <input id="cc-exp-year" autocomplete="cc-exp-year">
                </form>`,
-    addressFieldDetails: [
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name"},
-      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2"},
-    ],
-    creditCardFieldDetails: [
-      {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-name"},
-      {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-exp-month"},
-      {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-exp-year"},
-    ],
-    isValidForm: {
-      address: false,
-      creditCard: false,
-    },
+    addressFieldDetails: [],
+    creditCardFieldDetails: [],
+    validFieldDetails: [],
   },
   {
     description: "Three sets of adjacent phone number fields",
     document: `<form>
                  <input id="shippingAreaCode" autocomplete="shipping tel" maxlength="3">
                  <input id="shippingPrefix" autocomplete="shipping tel" maxlength="3">
                  <input id="shippingSuffix" autocomplete="shipping tel" maxlength="4">
                  <input id="shippingTelExt" autocomplete="shipping tel-extension">
@@ -181,20 +197,29 @@ const TESTCASES = [
       {"section": "", "addressType": "billing", "contactType": "", "fieldName": "tel-local-prefix"},
       {"section": "", "addressType": "billing", "contactType": "", "fieldName": "tel-local-suffix"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "tel-country-code"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "tel-area-code"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "tel-local-prefix"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "tel-local-suffix"},
     ],
     creditCardFieldDetails: [],
-    isValidForm: {
-      address: true,
-      creditCard: false,
-    },
+    validFieldDetails: [
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel-area-code"},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel-local-prefix"},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel-local-suffix"},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel-extension"},
+      {"section": "", "addressType": "billing", "contactType": "", "fieldName": "tel-area-code"},
+      {"section": "", "addressType": "billing", "contactType": "", "fieldName": "tel-local-prefix"},
+      {"section": "", "addressType": "billing", "contactType": "", "fieldName": "tel-local-suffix"},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "tel-country-code"},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "tel-area-code"},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "tel-local-prefix"},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "tel-local-suffix"},
+    ],
     ids: [
       "shippingAreaCode", "shippingPrefix", "shippingSuffix", "shippingTelExt",
       "billingAreaCode", "billingPrefix", "billingSuffix",
       "otherCountryCode", "otherAreaCode", "otherPrefix", "otherSuffix",
     ],
   },
   {
     description: "Dedup the same field names of the different telephone fields.",
@@ -211,20 +236,23 @@ const TESTCASES = [
     addressFieldDetails: [
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email"},
       {"section": "", "addressType": "", "contactType": "", "fieldName": "tel"},
     ],
     creditCardFieldDetails: [],
-    isValidForm: {
-      address: true,
-      creditCard: false,
-    },
+    validFieldDetails: [
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name"},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name"},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address"},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email"},
+      {"section": "", "addressType": "", "contactType": "", "fieldName": "tel"},
+    ],
     ids: ["i1", "i2", "i3", "i4", "homePhone"],
   },
   {
     description: "The duplicated phones of a single one and a set with ac, prefix, suffix.",
     document: `<form>
                  <input id="i1" autocomplete="shipping given-name">
                  <input id="i2" autocomplete="shipping family-name">
                  <input id="i3" autocomplete="shipping street-address">
@@ -244,63 +272,79 @@ const TESTCASES = [
       // this case. We can see if there is any better solution later.
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel"},
 
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel-area-code"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel-local-prefix"},
       {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel-local-suffix"},
     ],
     creditCardFieldDetails: [],
-    isValidForm: {
-      address: true,
-      creditCard: false,
-    },
+    validFieldDetails: [
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name"},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name"},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address"},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email"},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel"},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel-area-code"},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel-local-prefix"},
+      {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel-local-suffix"},
+    ],
     ids: ["i1", "i2", "i3", "i4", "singlePhone",
       "shippingAreaCode", "shippingPrefix", "shippingSuffix"],
   },
 ];
 
 for (let tc of TESTCASES) {
   (function() {
     let testcase = tc;
     add_task(async function() {
       do_print("Starting testcase: " + testcase.description);
 
       let doc = MockDocument.createTestDocument("http://localhost:8080/test/",
                                                 testcase.document);
       let form = doc.querySelector("form");
       let formLike = FormLikeFactory.createFromForm(form);
 
-      Array.of(
-        ...testcase.addressFieldDetails,
-        ...testcase.creditCardFieldDetails
-      ).forEach((detail, index) => {
-        let elementRef;
-        if (testcase.ids && testcase.ids[index]) {
-          elementRef = doc.getElementById(testcase.ids[index]);
-        } else {
-          elementRef = doc.querySelector("*[autocomplete*='" + detail.fieldName + "']");
+      function setElementWeakRef(details) {
+        if (!details) {
+          return;
         }
-        detail.elementWeakRef = Cu.getWeakReference(elementRef);
-      });
-      let handler = new FormAutofillHandler(formLike);
 
-      handler.collectFormFields();
+        details.forEach((detail, index) => {
+          let elementRef;
+          if (testcase.ids && testcase.ids[index]) {
+            elementRef = doc.getElementById(testcase.ids[index]);
+          } else {
+            elementRef = doc.querySelector("*[autocomplete*='" + detail.fieldName + "']");
+          }
+          detail.elementWeakRef = Cu.getWeakReference(elementRef);
+        });
+      }
 
       function verifyDetails(handlerDetails, testCaseDetails) {
+        if (handlerDetails === null) {
+          Assert.equal(handlerDetails, testCaseDetails);
+          return;
+        }
         Assert.equal(handlerDetails.length, testCaseDetails.length);
         handlerDetails.forEach((detail, index) => {
           Assert.equal(detail.fieldName, testCaseDetails[index].fieldName, "fieldName");
           Assert.equal(detail.section, testCaseDetails[index].section, "section");
           Assert.equal(detail.addressType, testCaseDetails[index].addressType, "addressType");
           Assert.equal(detail.contactType, testCaseDetails[index].contactType, "contactType");
           Assert.equal(detail.elementWeakRef.get(), testCaseDetails[index].elementWeakRef.get(), "DOM reference");
         });
       }
+      [
+        testcase.addressFieldDetails,
+        testcase.creditCardFieldDetails,
+        testcase.validFieldDetails,
+      ].forEach(details => setElementWeakRef(details));
 
-      verifyDetails(handler.addressFieldDetails, testcase.addressFieldDetails);
-      verifyDetails(handler.creditCardFieldDetails, testcase.creditCardFieldDetails);
+      let handler = new FormAutofillHandler(formLike);
+      let validFieldDetails = handler.collectFormFields();
 
-      Assert.equal(handler.isValidAddressForm, testcase.isValidForm.address, "Valid Address Form Checking");
-      Assert.equal(handler.isValidCreditCardForm, testcase.isValidForm.creditCard, "Valid Credit Card Form Checking");
+      verifyDetails(handler.address.fieldDetails, testcase.addressFieldDetails);
+      verifyDetails(handler.creditCard.fieldDetails, testcase.creditCardFieldDetails);
+      verifyDetails(validFieldDetails, testcase.validFieldDetails);
     });
   })();
 }
--- a/browser/extensions/formautofill/test/unit/test_onFormSubmitted.js
+++ b/browser/extensions/formautofill/test/unit/test_onFormSubmitted.js
@@ -4,62 +4,134 @@ Cu.import("resource://formautofill/FormA
 
 const MOCK_DOC = MockDocument.createTestDocument("http://localhost:8080/test/",
                    `<form id="form1">
                       <input id="street-addr" autocomplete="street-address">
                       <input id="city" autocomplete="address-level2">
                       <input id="country" autocomplete="country">
                       <input id="email" autocomplete="email">
                       <input id="tel" autocomplete="tel">
+                      <input id="cc-name" autocomplete="cc-name">
+                      <input id="cc-number" autocomplete="cc-number">
+                      <input id="cc-exp-month" autocomplete="cc-exp-month">
+                      <input id="cc-exp-year" autocomplete="cc-exp-year">
                       <input id="submit" type="submit">
                     </form>`);
 const TARGET_ELEMENT_ID = "street-addr";
 
 const TESTCASES = [
   {
-    description: "Should not trigger saving if the number of fields is less than 3",
+    description: "Should not trigger address saving if the number of fields is less than 3",
     formValue: {
       "street-addr": "331 E. Evelyn Avenue",
       "tel": "1-650-903-0800",
     },
     expectedResult: {
       formSubmission: false,
     },
   },
   {
-    description: "Trigger profile saving",
+    description: "Should not trigger credit card saving if number is empty",
+    formValue: {
+      "cc-name": "John Doe",
+      "cc-exp-month": 12,
+      "cc-exp-year": 2000,
+    },
+    expectedResult: {
+      formSubmission: false,
+    },
+  },
+  {
+    description: "Trigger address saving",
     formValue: {
       "street-addr": "331 E. Evelyn Avenue",
       "country": "USA",
       "tel": "1-650-903-0800",
     },
     expectedResult: {
       formSubmission: true,
-      profile: {
+      records: {
         address: {
           guid: null,
           record: {
             "street-address": "331 E. Evelyn Avenue",
             "country": "USA",
             "tel": "1-650-903-0800",
           },
         },
       },
     },
   },
   {
+    description: "Trigger credit card saving",
+    formValue: {
+      "cc-name": "John Doe",
+      "cc-number": "1234567812345678",
+      "cc-exp-month": 12,
+      "cc-exp-year": 2000,
+    },
+    expectedResult: {
+      formSubmission: true,
+      records: {
+        creditCard: {
+          guid: null,
+          record: {
+            "cc-name": "John Doe",
+            "cc-number": "1234567812345678",
+            "cc-exp-month": 12,
+            "cc-exp-year": 2000,
+          },
+        },
+      },
+    },
+  },
+  {
+    description: "Trigger address and credit card saving",
+    formValue: {
+      "street-addr": "331 E. Evelyn Avenue",
+      "country": "USA",
+      "tel": "1-650-903-0800",
+      "cc-name": "John Doe",
+      "cc-number": "1234567812345678",
+      "cc-exp-month": 12,
+      "cc-exp-year": 2000,
+    },
+    expectedResult: {
+      formSubmission: true,
+      records: {
+        address: {
+          guid: null,
+          record: {
+            "street-address": "331 E. Evelyn Avenue",
+            "country": "USA",
+            "tel": "1-650-903-0800",
+          },
+        },
+        creditCard: {
+          guid: null,
+          record: {
+            "cc-name": "John Doe",
+            "cc-number": "1234567812345678",
+            "cc-exp-month": 12,
+            "cc-exp-year": 2000,
+          },
+        },
+      },
+    },
+  },
+  {
     description: "Profile saved with trimmed string",
     formValue: {
       "street-addr": "331 E. Evelyn Avenue  ",
       "country": "USA",
       "tel": "  1-650-903-0800",
     },
     expectedResult: {
       formSubmission: true,
-      profile: {
+      records: {
         address: {
           guid: null,
           record: {
             "street-address": "331 E. Evelyn Avenue",
             "country": "USA",
             "tel": "1-650-903-0800",
           },
         },
@@ -71,17 +143,17 @@ const TESTCASES = [
     formValue: {
       "street-addr": "331 E. Evelyn Avenue",
       "country": "USA",
       "email": "  ",
       "tel": "1-650-903-0800",
     },
     expectedResult: {
       formSubmission: true,
-      profile: {
+      records: {
         address: {
           guid: null,
           record: {
             "street-address": "331 E. Evelyn Avenue",
             "country": "USA",
             "tel": "1-650-903-0800",
           },
         },
@@ -96,31 +168,32 @@ add_task(async function handle_earlyform
   sinon.spy(FormAutofillContent, "_onFormSubmit");
 
   do_check_eq(FormAutofillContent.notify(fakeForm), true);
   do_check_eq(FormAutofillContent._onFormSubmit.called, false);
   FormAutofillContent._onFormSubmit.restore();
 });
 
 TESTCASES.forEach(testcase => {
-  add_task(async function check_profile_saving_is_called_correctly() {
+  add_task(async function check_records_saving_is_called_correctly() {
     do_print("Starting testcase: " + testcase.description);
 
     let form = MOCK_DOC.getElementById("form1");
+    form.reset();
     for (let key in testcase.formValue) {
       let input = MOCK_DOC.getElementById(key);
       input.value = testcase.formValue[key];
     }
     sinon.stub(FormAutofillContent, "_onFormSubmit");
 
     let element = MOCK_DOC.getElementById(TARGET_ELEMENT_ID);
     FormAutofillContent.identifyAutofillFields(element);
     FormAutofillContent.notify(form);
 
     do_check_eq(FormAutofillContent._onFormSubmit.called,
                 testcase.expectedResult.formSubmission);
     if (FormAutofillContent._onFormSubmit.called) {
       Assert.deepEqual(FormAutofillContent._onFormSubmit.args[0][0],
-                       testcase.expectedResult.profile);
+                       testcase.expectedResult.records);
     }
     FormAutofillContent._onFormSubmit.restore();
   });
 });
--- a/browser/extensions/onboarding/bootstrap.js
+++ b/browser/extensions/onboarding/bootstrap.js
@@ -55,76 +55,95 @@ function setPrefs(prefs) {
   prefs.forEach(pref => {
     if (PREF_WHITELIST.includes(pref.name)) {
       Preferences.set(pref.name, pref.value);
     }
   });
 }
 
 /**
+ * syncTourChecker listens to and maintains the login status inside, and can be
+ * queried at any time once initialized.
+ */
+let syncTourChecker = {
+  _registered: false,
+  _loggedIn: false,
+
+  isLoggedIn() {
+    return this._loggedIn;
+  },
+
+  observe(subject, topic) {
+    switch (topic) {
+      case "fxaccounts:onlogin":
+        this.setComplete();
+        break;
+      case "fxaccounts:onlogout":
+        this._loggedIn = false;
+        break;
+    }
+  },
+
+  init() {
+    // Check if we've already logged in at startup.
+    fxAccounts.getSignedInUser().then(user => {
+      if (user) {
+        this.setComplete();
+      }
+      // Observe for login action if we haven't logged in yet.
+      this.register();
+    });
+  },
+
+  register() {
+    if (this._registered) {
+      return;
+    }
+    Services.obs.addObserver(this, "fxaccounts:onlogin");
+    Services.obs.addObserver(this, "fxaccounts:onlogout");
+    this._registered = true;
+  },
+
+  setComplete() {
+    this._loggedIn = true;
+    Services.prefs.setBoolPref("browser.onboarding.tour.onboarding-tour-sync.completed", true);
+  },
+
+  unregister() {
+    if (!this._registered) {
+      return;
+    }
+    Services.obs.removeObserver(this, "fxaccounts:onlogin");
+    Services.obs.removeObserver(this, "fxaccounts:onlogout");
+    this._registered = false;
+  },
+
+  uninit() {
+    this.unregister();
+  },
+}
+
+/**
  * Listen and process events from content.
  */
 function initContentMessageListener() {
   Services.mm.addMessageListener("Onboarding:OnContentMessage", msg => {
     switch (msg.data.action) {
       case "set-prefs":
         setPrefs(msg.data.params);
         break;
+      case "get-login-status":
+        msg.target.messageManager.sendAsyncMessage("Onboarding:ResponseLoginStatus", {
+          isLoggedIn: syncTourChecker.isLoggedIn()
+        });
+        break;
     }
   });
 }
 
-let syncTourChecker = {
-  registered: false,
-
-  observe() {
-    this.setComplete();
-  },
-
-  init() {
-    if (Services.prefs.getBoolPref("browser.onboarding.tour.onboarding-tour-sync.completed", false)) {
-      return;
-    }
-    // Check if we've already logged in at startup.
-    fxAccounts.getSignedInUser().then(user => {
-      if (user) {
-        this.setComplete();
-        return;
-      }
-      // Observe for login action if we haven't logged in yet.
-      this.register();
-    });
-  },
-
-  register() {
-    if (this.registered) {
-      return;
-    }
-    Services.obs.addObserver(this, "fxaccounts:onverified");
-    this.registered = true;
-  },
-
-  setComplete() {
-    Services.prefs.setBoolPref("browser.onboarding.tour.onboarding-tour-sync.completed", true);
-    this.unregister();
-  },
-
-  unregister() {
-    if (!this.registered) {
-      return;
-    }
-    Services.obs.removeObserver(this, "fxaccounts:onverified");
-    this.registered = false;
-  },
-
-  uninit() {
-    this.unregister();
-  },
-}
-
 /**
  * onBrowserReady - Continues startup of the add-on after browser is ready.
  */
 function onBrowserReady() {
   waitingForBrowserReady = false;
 
   OnboardingTourType.check();
   Services.mm.loadFrameScript("resource://onboarding/onboarding.js", true);
--- a/browser/extensions/onboarding/content/onboarding.css
+++ b/browser/extensions/onboarding/content/onboarding.css
@@ -56,17 +56,19 @@
   border-radius: 22px;
   padding: 5px 8px;
   width: 110px;
   margin-inline-start: 3px;
   margin-top: -5px;
 }
 
 #onboarding-overlay-dialog,
-.onboarding-hidden {
+.onboarding-hidden,
+#onboarding-tour-sync-page[data-login-state=logged-in] .show-on-logged-out,
+#onboarding-tour-sync-page[data-login-state=logged-out] .show-on-logged-in {
   display: none;
 }
 
 .onboarding-close-btn {
    position: absolute;
    top: 15px;
    offset-inline-end: 15px;
    cursor: pointer;
--- a/browser/extensions/onboarding/content/onboarding.js
+++ b/browser/extensions/onboarding/content/onboarding.js
@@ -189,42 +189,57 @@ var onboardingTourset = {
     getNotificationStrings(bundle) {
       return {
         title: bundle.GetStringFromName("onboarding.notification.onboarding-tour-sync.title"),
         message: bundle.GetStringFromName("onboarding.notification.onboarding-tour-sync.message"),
         button: bundle.GetStringFromName("onboarding.button.learnMore"),
       };
     },
     getPage(win, bundle) {
+      const STATE_LOGOUT = "logged-out";
+      const STATE_LOGIN = "logged-in";
       let div = win.document.createElement("div");
       div.classList.add("onboarding-no-button");
+      div.dataset.loginState = STATE_LOGOUT;
       // The email validation pattern used in the form comes from IETF rfc5321,
       // which is identical to server-side checker of Firefox Account. See
       // discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1378770#c6
       // for detail.
       let emailRegex = "^[\\w.!#$%&’*+\\/=?^`{|}~-]{1,64}@[a-z\\d](?:[a-z\\d-]{0,253}[a-z\\d])?(?:\\.[a-z\\d](?:[a-z\\d-]{0,253}[a-z\\d])?)+$";
       div.innerHTML = `
         <section class="onboarding-tour-description">
-          <h1 data-l10n-id="onboarding.tour-sync.title2"></h1>
-          <p data-l10n-id="onboarding.tour-sync.description2"></p>
+          <h1 data-l10n-id="onboarding.tour-sync.title2" class="show-on-logged-out"></h1>
+          <p data-l10n-id="onboarding.tour-sync.description2" class="show-on-logged-out"></p>
+          <h1 data-l10n-id="onboarding.tour-sync.logged-in.title" class="show-on-logged-in"></h1>
+          <p data-l10n-id="onboarding.tour-sync.logged-in.description" class="show-on-logged-in"></p>
         </section>
         <section class="onboarding-tour-content">
-          <form>
+          <form class="show-on-logged-out">
             <h3 data-l10n-id="onboarding.tour-sync.form.title"></h3>
             <p data-l10n-id="onboarding.tour-sync.form.description"></p>
             <input id="onboarding-tour-sync-email-input" type="email" required="true"></input><br />
             <button id="onboarding-tour-sync-button" class="onboarding-tour-action-button" data-l10n-id="onboarding.tour-sync.button"></button>
           </form>
           <img src="resource://onboarding/img/figure_sync.svg" role="presentation"/>
         </section>
       `;
       let emailInput = div.querySelector("#onboarding-tour-sync-email-input");
       emailInput.placeholder =
         bundle.GetStringFromName("onboarding.tour-sync.email-input.placeholder");
       emailInput.pattern = emailRegex;
+
+      div.addEventListener("beforeshow", () => {
+        function loginStatusListener(msg) {
+          removeMessageListener("Onboarding:ResponseLoginStatus", loginStatusListener);
+          div.dataset.loginState = msg.data.isLoggedIn ? STATE_LOGIN : STATE_LOGOUT;
+        }
+        sendMessageToChrome("get-login-status");
+        addMessageListener("Onboarding:ResponseLoginStatus", loginStatusListener);
+      });
+
       return div;
     },
   },
   "library": {
     id: "onboarding-tour-library",
     tourNameId: "onboarding.tour-library",
     getNotificationStrings(bundle) {
       return {
@@ -301,16 +316,25 @@ var onboardingTourset = {
         </section>
       `;
       return div;
     },
   },
 };
 
 /**
+ * @param {String} action the action to ask the chrome to do
+ * @param {Array} params the parameters for the action
+ */
+function sendMessageToChrome(action, params) {
+  sendAsyncMessage("Onboarding:OnContentMessage", {
+    action, params
+  });
+}
+/**
  * The script won't be initialized if we turned off onboarding by
  * setting "browser.onboarding.enabled" to false.
  */
 class Onboarding {
   constructor(contentWindow) {
     this.init(contentWindow);
   }
 
@@ -429,26 +453,16 @@ class Onboarding {
     if (this._prefsObserved) {
       for (let [name, callback] of this._prefsObserved) {
         Preferences.ignore(name, callback);
       }
       this._prefsObserved = null;
     }
   }
 
-  /**
-   * @param {String} action the action to ask the chrome to do
-   * @param {Array} params the parameters for the action
-   */
-  sendMessageToChrome(action, params) {
-    sendAsyncMessage("Onboarding:OnContentMessage", {
-      action, params
-    });
-  }
-
   handleEvent(evt) {
     if (evt.type === "resize") {
       this._window.cancelIdleCallback(this._resizeTimerId);
       this._resizeTimerId =
         this._window.requestIdleCallback(() => this._resizeUI());
 
       return;
     }
@@ -520,17 +534,22 @@ class Onboarding {
     if (hiddenCheckbox.checked) {
       this.hide();
     }
   }
 
   gotoPage(tourId) {
     let targetPageId = `${tourId}-page`;
     for (let page of this._tourPages) {
-      page.style.display = page.id != targetPageId ? "none" : "";
+      if (page.id === targetPageId) {
+        page.style.display = "";
+        page.dispatchEvent(new this._window.CustomEvent("beforeshow"));
+      } else {
+        page.style.display = "none";
+      }
     }
     for (let li of this._tourItems) {
       if (li.id == tourId) {
         li.classList.add("onboarding-active");
       } else {
         li.classList.remove("onboarding-active");
       }
     }
@@ -546,17 +565,17 @@ class Onboarding {
       if (!this.isTourCompleted(id)) {
         params.push({
           name: `browser.onboarding.tour.${id}.completed`,
           value: true
         });
       }
     });
     if (params.length > 0) {
-      this.sendMessageToChrome("set-prefs", params);
+      sendMessageToChrome("set-prefs", params);
     }
   }
 
   markTourCompletionState(tourId) {
     // We are doing lazy load so there might be no items.
     if (this._tourItems && this._tourItems.length > 0 && this.isTourCompleted(tourId)) {
       let targetItem = this._tourItems.find(item => item.id == tourId);
       targetItem.classList.add("onboarding-complete");
@@ -574,17 +593,17 @@ class Onboarding {
       // Don't mute when this is set to 0 on purpose.
       return false;
     }
 
     // Reuse the `last-time-of-changing-tour-sec` to save the time that
     // we try to prompt on the 1st session.
     let lastTime = 1000 * Preferences.get("browser.onboarding.notification.last-time-of-changing-tour-sec", 0);
     if (lastTime <= 0) {
-      this.sendMessageToChrome("set-prefs", [{
+      sendMessageToChrome("set-prefs", [{
         name: "browser.onboarding.notification.last-time-of-changing-tour-sec",
         value: Math.floor(Date.now() / 1000)
       }]);
       return true;
     }
     return Date.now() - lastTime <= muteDuration;
   }
 
@@ -614,34 +633,34 @@ class Onboarding {
     params.push({
       name: "browser.onboarding.notification.last-time-of-changing-tour-sec",
       value: 0
     });
     params.push({
       name: "browser.onboarding.notification.prompt-count",
       value: 0
     });
-    this.sendMessageToChrome("set-prefs", params);
+    sendMessageToChrome("set-prefs", params);
   }
 
   _getNotificationQueue() {
     let queue = "";
     if (Preferences.isSet("browser.onboarding.notification.tour-ids-queue")) {
       queue = Preferences.get("browser.onboarding.notification.tour-ids-queue");
     } else {
       // For each tour, it only gets 2 chances to prompt with notification
       // (each chance includes 8 impressions or 5-days max life time)
       // if user never interact with it.
       // Assume there are tour #0 ~ #5. Here would form the queue as
       // "#0,#1,#2,#3,#4,#5,#0,#1,#2,#3,#4,#5".
       // Then we would loop through this queue and remove prompted tour from the queue
       // until the queue is empty.
       let ids = this._tours.map(tour => tour.id).join(",");
       queue = `${ids},${ids}`;
-      this.sendMessageToChrome("set-prefs", [{
+      sendMessageToChrome("set-prefs", [{
         name: "browser.onboarding.notification.tour-ids-queue",
         value: queue
       }]);
     }
     return queue ? queue.split(",") : [];
   }
 
   showNotification() {
@@ -660,17 +679,17 @@ class Onboarding {
       queue.shift();
     }
     // We don't want to prompt completed tour.
     while (queue.length > 0 && this.isTourCompleted(queue[0])) {
       queue.shift();
     }
 
     if (queue.length == 0) {
-      this.sendMessageToChrome("set-prefs", [
+      sendMessageToChrome("set-prefs", [
         {
           name: "browser.onboarding.notification.finished",
           value: true
         },
         {
           name: "browser.onboarding.notification.tour-ids-queue",
           value: ""
         }
@@ -711,17 +730,17 @@ class Onboarding {
       });
     } else {
       let promptCount = Preferences.get(PROMPT_COUNT_PREF, 0);
       params.push({
         name: PROMPT_COUNT_PREF,
         value: promptCount + 1
       });
     }
-    this.sendMessageToChrome("set-prefs", params);
+    sendMessageToChrome("set-prefs", params);
   }
 
   hideNotification() {
     if (this._notificationBar) {
       this._notificationBar.classList.remove("onboarding-opened");
     }
   }
 
@@ -751,17 +770,17 @@ class Onboarding {
     let closeBtn = div.querySelector("#onboarding-notification-close-btn");
     closeBtn.setAttribute("title",
       this._bundle.GetStringFromName("onboarding.notification-close-button-tooltip"));
     return div;
   }
 
   hide() {
     this.setToursCompleted(this._tours.map(tour => tour.id));
-    this.sendMessageToChrome("set-prefs", [
+    sendMessageToChrome("set-prefs", [
       {
         name: "browser.onboarding.hidden",
         value: true
       },
       {
         name: "browser.onboarding.notification.finished",
         value: true
       }
--- a/browser/extensions/onboarding/locales/en-US/onboarding.properties
+++ b/browser/extensions/onboarding/locales/en-US/onboarding.properties
@@ -72,16 +72,19 @@ onboarding.tour-default-browser.is-defau
 # LOCALIZATION NOTE(onboarding.notification.onboarding-tour-default-browser.title): This string will be used in the notification title for the default browser tour. %S is brandShortName.
 onboarding.notification.onboarding-tour-default-browser.title=Make %S your go-to browser.
 # LOCALIZATION NOTE(onboarding.notification.onboarding-tour-default-browser.message): This string will be used in the notification message for the default browser tour. %1$S is brandShortName
 onboarding.notification.onboarding-tour-default-browser.message=It doesn’t take much to get the most from %1$S. Just set %1$S as your default browser and put control, customization, and protection on autopilot.
 
 onboarding.tour-sync2=Sync
 onboarding.tour-sync.title2=Pick up where you left off.
 onboarding.tour-sync.description2=Sync makes it easy to access bookmarks, passwords, and even open tabs on all your devices. Sync also gives you control of the types of information you want, and don’t want, to share.
+onboarding.tour-sync.logged-in.title=You’re signed in to Sync!
+# LOCALIZATION NOTE(onboarding.tour-sync.logged-in.description): %1$S is brandShortName.
+onboarding.tour-sync.logged-in.description=Sync works when you’re signed in to %1$S on more than one device. Have a mobile device? Install the %1$S app and sign in to get your bookmarks, history, and passwords on the go.
 # LOCALIZATION NOTE(onboarding.tour-sync.form.title): This string is displayed
 # as a title and followed by onboarding.tour-sync.form.description.
 # Your translation should be consistent with the form displayed in
 # about:accounts when signing up to Firefox Account.
 onboarding.tour-sync.form.title=Create a Firefox Account
 # LOCALIZATION NOTE(onboarding.tour-sync.form.description): The description
 # continues after onboarding.tour-sync.form.title to create a complete sentence.
 # If it's not possible for your locale, you can translate this string as
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -983,12 +983,13 @@ you can use these alternative items. Oth
 <!ENTITY updateRestart.header.message2 "Restart to update &brandShorterName;.">
 <!ENTITY updateRestart.acceptButton.label "Restart and Restore">
 <!ENTITY updateRestart.acceptButton.accesskey "R">
 <!ENTITY updateRestart.cancelButton.label "Not Now">
 <!ENTITY updateRestart.cancelButton.accesskey "N">
 <!ENTITY updateRestart.panelUI.label2 "Restart to update &brandShorterName;">
 
 <!ENTITY pageActionButton.tooltip "Page actions">
+<!ENTITY pageAction.addToUrlbar.label "Add to Address Bar">
+<!ENTITY pageAction.removeFromUrlbar.label "Remove from Address Bar">
 
 <!ENTITY sendToDevice.label2 "Send to Device">
-<!ENTITY sendToDevice.viewTitle "Send to Device">
 <!ENTITY sendToDevice.syncNotReady.label "Syncing Devices…">
new file mode 100644
--- /dev/null
+++ b/browser/modules/PageActions.jsm
@@ -0,0 +1,935 @@
+/* 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 = [
+  "PageActions",
+  // PageActions.Action
+  // PageActions.Button
+  // PageActions.Subview
+  // PageActions.ACTION_ID_BOOKMARK_SEPARATOR
+  // PageActions.ACTION_ID_BUILT_IN_SEPARATOR
+];
+
+const { utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "BinarySearch",
+  "resource://gre/modules/BinarySearch.jsm");
+
+
+const ACTION_ID_BOOKMARK_SEPARATOR = "bookmarkSeparator";
+const ACTION_ID_BUILT_IN_SEPARATOR = "builtInSeparator";
+
+const PREF_PERSISTED_ACTIONS = "browser.pageActions.persistedActions";
+
+
+this.PageActions = {
+  /**
+   * Inits.  Call to init.
+   */
+  init() {
+    let callbacks = this._deferredAddActionCalls;
+    delete this._deferredAddActionCalls;
+
+    this._loadPersistedActions();
+
+    // Add the built-in actions, which are defined below in this file.
+    for (let options of gBuiltInActions) {
+      if (options._isSeparator || !this.actionForID(options.id)) {
+        this.addAction(new Action(options));
+      }
+    }
+
+    // These callbacks are deferred until init happens and all built-in actions
+    // are added.
+    while (callbacks && callbacks.length) {
+      callbacks.shift()();
+    }
+  },
+
+  _deferredAddActionCalls: [],
+
+  /**
+   * The list of Action objects, sorted in the order in which they should be
+   * placed in the page action panel.  If there are both built-in and non-built-
+   * in actions, then the list will include the separator between the two.  The
+   * list is not live.  (array of Action objects)
+   */
+  get actions() {
+    let actions = this.builtInActions;
+    if (this.nonBuiltInActions.length) {
+      // There are non-built-in actions, so include them too.  Add a separator
+      // between the built-ins and non-built-ins so that the returned array
+      // looks like: [...built-ins, separator, ...non-built-ins]
+      actions.push(new Action({
+        id: ACTION_ID_BUILT_IN_SEPARATOR,
+        _isSeparator: true,
+      }));
+      actions.push(...this.nonBuiltInActions);
+    }
+    return actions;
+  },
+
+  /**
+   * The list of built-in actions.  Not live.  (array of Action objects)
+   */
+  get builtInActions() {
+    return this._builtInActions.slice();
+  },
+
+  /**
+   * The list of non-built-in actions.  Not live.  (array of Action objects)
+   */
+  get nonBuiltInActions() {
+    return this._nonBuiltInActions.slice();
+  },
+
+  /**
+   * Gets an action.
+   *
+   * @param  id (string, required)
+   *         The ID of the action to get.
+   * @return The Action object, or null if none.
+   */
+  actionForID(id) {
+    return this._actionsByID.get(id);
+  },
+
+  /**
+   * Registers an action.
+   *
+   * Actions are registered by their IDs.  An error is thrown if an action with
+   * the given ID has already been added.  Use actionForID() before calling this
+   * method if necessary.
+   *
+   * Be sure to call remove() on the action if the lifetime of the code that
+   * owns it is shorter than the browser's -- if it lives in an extension, for
+   * example.
+   *
+   * @param  action (Action, required)
+   *         The Action object to register.
+   * @return The given Action.
+   */
+  addAction(action) {
+    if (this._deferredAddActionCalls) {
+      // init() hasn't been called yet.  Defer all additions until it's called,
+      // at which time _deferredAddActionCalls will be deleted.
+      this._deferredAddActionCalls.push(() => this.addAction(action));
+      return action;
+    }
+
+    // The IDs of the actions in the panel and urlbar before which the new
+    // action shoud be inserted.  null means at the end, or it's irrelevant.
+    let panelInsertBeforeID = null;
+    let urlbarInsertBeforeID = null;
+
+    let placeBuiltInSeparator = false;
+
+    if (action.__isSeparator) {
+      this._builtInActions.push(action);
+    } else {
+      if (this.actionForID(action.id)) {
+        throw new Error(`An Action with ID '${action.id}' has already been added.`);
+      }
+      this._actionsByID.set(action.id, action);
+
+      // Insert the action into the appropriate list, either _builtInActions or
+      // _nonBuiltInActions, and find panelInsertBeforeID.
+
+      // Keep in mind that _insertBeforeActionID may be present but null, which
+      // means the action should be appended to the built-ins.
+      if ("__insertBeforeActionID" in action) {
+        // A "semi-built-in" action, probably an action from an extension
+        // bundled with the browser.  Right now we simply assume that no other
+        // consumers will use _insertBeforeActionID.
+        let index =
+          !action.__insertBeforeActionID ? -1 :
+          this._builtInActions.findIndex(a => {
+            return a.id == action.__insertBeforeActionID;
+          });
+        if (index < 0) {
+          // Append the action.
+          index = this._builtInActions.length;
+          if (this._nonBuiltInActions.length) {
+            panelInsertBeforeID = ACTION_ID_BUILT_IN_SEPARATOR;
+          }
+        } else {
+          panelInsertBeforeID = this._builtInActions[index].id;
+        }
+        this._builtInActions.splice(index, 0, action);
+      } else if (gBuiltInActions.find(a => a.id == action.id)) {
+        // A built-in action.  These are always added on init before all other
+        // actions, one after the other, so just push onto the array.
+        this._builtInActions.push(action);
+        if (this._nonBuiltInActions.length) {
+          panelInsertBeforeID = ACTION_ID_BUILT_IN_SEPARATOR;
+        }
+      } else {
+        // A non-built-in action, like a non-bundled extension potentially.
+        // Keep this list sorted by title.
+        let index = BinarySearch.insertionIndexOf((a1, a2) => {
+          return a1.title.localeCompare(a2.title);
+        }, this._nonBuiltInActions, action);
+        if (index < this._nonBuiltInActions.length) {
+          panelInsertBeforeID = this._nonBuiltInActions[index].id;
+        }
+        // If this is the first non-built-in, then the built-in separator must
+        // be placed between the built-ins and non-built-ins.
+        if (!this._nonBuiltInActions.length) {
+          placeBuiltInSeparator = true;
+        }
+        this._nonBuiltInActions.splice(index, 0, action);
+      }
+
+      if (this._persistedActions.ids[action.id]) {
+        // The action has been seen before.  Override its shownInUrlbar value
+        // with the persisted value.  Set the private version of that property
+        // so that onActionToggledShownInUrlbar isn't called, which happens when
+        // the public version is set.
+        action._shownInUrlbar =
+          this._persistedActions.idsInUrlbar.includes(action.id);
+      } else {
+        // The action is new.  Store it in the persisted actions.
+        this._persistedActions.ids[action.id] = true;
+        if (action.shownInUrlbar) {
+          this._persistedActions.idsInUrlbar.push(action.id);
+        }
+        this._storePersistedActions();
+      }
+
+      if (action.shownInUrlbar) {
+        urlbarInsertBeforeID = this.insertBeforeActionIDInUrlbar(action);
+      }
+    }
+
+    for (let win of browserWindows()) {
+      if (placeBuiltInSeparator) {
+        let sep = new Action({
+          id: ACTION_ID_BUILT_IN_SEPARATOR,
+          _isSeparator: true,
+        });
+        browserPageActions(win).placeAction(sep, null, null);
+      }
+      browserPageActions(win).placeAction(action, panelInsertBeforeID,
+                                          urlbarInsertBeforeID);
+    }
+
+    return action;
+  },
+
+  _builtInActions: [],
+  _nonBuiltInActions: [],
+  _actionsByID: new Map(),
+
+  /**
+   * Returns the ID of the action among the current registered actions in the
+   * urlbar before which the given action should be inserted, ignoring whether
+   * the given action's shownInUrlbar is true or false.
+   *
+   * @return The ID of the action before which the given action should be
+   *         inserted.  If the given action should be inserted last or it should
+   *         not be inserted at all, returns null.
+   */
+  insertBeforeActionIDInUrlbar(action) {
+    // First, find the index of the given action.
+    let idsInUrlbar = this._persistedActions.idsInUrlbar;
+    let index = idsInUrlbar.indexOf(action.id);
+    if (index < 0) {
+      return null;
+    }
+    // Now start at the next index and find the ID of the first action that's
+    // currently registered.  Remember that IDs in idsInUrlbar may belong to
+    // actions that aren't currently registered.
+    for (let i = index + 1; i < idsInUrlbar.length; i++) {
+      let id = idsInUrlbar[i];
+      if (this.actionForID(id)) {
+        return id;
+      }
+    }
+    return null;
+  },
+
+  /**
+   * Call this when an action is removed.
+   *
+   * @param  action (Action object, required)
+   *         The action that was removed.
+   */
+  onActionRemoved(action) {
+    if (!this.actionForID(action.id)) {
+      // The action isn't present.  Not an error.
+      return;
+    }
+
+    this._actionsByID.delete(action.id);
+    for (let list of [this._nonBuiltInActions, this._builtInActions]) {
+      let index = list.findIndex(a => a.id == action.id);
+      if (index >= 0) {
+        list.splice(index, 1);
+        break;
+      }
+    }
+
+    // Remove the action from persisted storage.
+    delete this._persistedActions.ids[action.id];
+    let index = this._persistedActions.idsInUrlbar.indexOf(action.id);
+    if (index >= 0) {
+      this._persistedActions.idsInUrlbar.splice(index, 1);
+    }
+    this._storePersistedActions();
+
+    for (let win of browserWindows()) {
+      browserPageActions(win).removeAction(action);
+    }
+  },
+
+  /**
+   * Call this when an action's iconURL changes.
+   *
+   * @param  action (Action object, required)
+   *         The action whose iconURL property changed.
+   */
+  onActionSetIconURL(action) {
+    if (!this.actionForID(action.id)) {
+      // This may be called before the action has been added.
+      return;
+    }
+    for (let win of browserWindows()) {
+      browserPageActions(win).updateActionIconURL(action);
+    }
+  },
+
+  /**
+   * Call this when an action's title changes.
+   *
+   * @param  action (Action object, required)
+   *         The action whose title property changed.
+   */
+  onActionSetTitle(action) {
+    if (!this.actionForID(action.id)) {
+      // This may be called before the action has been added.
+      return;
+    }
+    for (let win of browserWindows()) {
+      browserPageActions(win).updateActionTitle(action);
+    }
+  },
+
+  /**
+   * Call this when an action's shownInUrlbar property changes.
+   *
+   * @param  action (Action object, required)
+   *         The action whose shownInUrlbar property changed.
+   */
+  onActionToggledShownInUrlbar(action) {
+    if (!this.actionForID(action.id)) {
+      // This may be called before the action has been added.
+      return;
+    }
+
+    // Update persisted storage.
+    let index = this._persistedActions.idsInUrlbar.indexOf(action.id);
+    if (action.shownInUrlbar) {
+      if (index < 0) {
+        this._persistedActions.idsInUrlbar.push(action.id);
+      }
+    } else if (index >= 0) {
+      this._persistedActions.idsInUrlbar.splice(index, 1);
+    }
+    this._storePersistedActions();
+
+    let insertBeforeID = this.insertBeforeActionIDInUrlbar(action);
+    for (let win of browserWindows()) {
+      browserPageActions(win).placeActionInUrlbar(action, insertBeforeID);
+    }
+  },
+
+  _storePersistedActions() {
+    let json = JSON.stringify(this._persistedActions);
+    Services.prefs.setStringPref(PREF_PERSISTED_ACTIONS, json);
+  },
+
+  _loadPersistedActions() {
+    try {
+      let json = Services.prefs.getStringPref(PREF_PERSISTED_ACTIONS);
+      this._persistedActions = JSON.parse(json);
+    } catch (ex) {}
+  },
+
+  _persistedActions: {
+    // action ID => true, for actions that have ever been seen and not removed
+    ids: {},
+    // action IDs ordered by position in urlbar
+    idsInUrlbar: [],
+  },
+};
+
+/**
+ * A single page action.
+ *
+ * @param  options (object, required)
+ *         An object with the following properties:
+ *         @param id (string, required)
+ *                The action's ID.  Treat this like the ID of a DOM node.
+ *         @param title (string, required)
+ *                The action's title.
+ *         @param iconURL (string, optional)
+ *                The URL of the action's icon.  Usually you want to specify an
+ *                icon in CSS, but this option is useful if that would be a pain
+ *                for some reason -- like your code is in an embedded
+ *                WebExtension.
+ *         @param nodeAttributes (object, optional)
+ *                An object of name-value pairs.  Each pair will be added as
+ *                an attribute to DOM nodes created for this action.
+ *         @param onCommand (function, optional)
+ *                Called when the action is clicked, but only if it has neither
+ *                a subview nor an iframe.  Passed the following arguments:
+ *                * event: The triggering event.
+ *                * buttonNode: The button node that was clicked.
+ *         @param onIframeShown (function, optional)
+ *                Called when the action's iframe is shown to the user.  Passed
+ *                the following arguments:
+ *                * iframeNode: The iframe.
+ *                * parentPanelNode: The panel node in which the iframe is
+ *                  shown.
+ *         @param onPlacedInPanel (function, optional)
+ *                Called when the action is added to the page action panel in
+ *                a browser window.  Passed the following arguments:
+ *                * buttonNode: The action's node in the page action panel.
+ *         @param onPlacedInUrlbar (function, optional)
+ *                Called when the action is added to the urlbar in a browser
+ *                window.  Passed the following arguments:
+ *                * buttonNode: The action's node in the urlbar.
+ *         @param onShowingInPanel (function, optional)
+ *                Called when a browser window's page action panel is showing.
+ *                Passed the following arguments:
+ *                * buttonNode: The action's node in the page action panel.
+ *         @param shownInUrlbar (bool, optional)
+ *                Pass true to show the action in the urlbar, false otherwise.
+ *                False by default.
+ *         @param subview (object, optional)
+ *                An options object suitable for passing to the Subview
+ *                constructor, if you'd like the action to have a subview.  See
+ *                the subview constructor for info on this object's properties.
+ *         @param tooltip (string, optional)
+ *                The action's button tooltip text.
+ *         @param urlbarIDOverride (string, optional)
+ *                Usually the ID of the action's button in the urlbar will be
+ *                generated automatically.  Pass a string for this property to
+ *                override that with your own ID.
+ *         @param wantsIframe (bool, optional)
+ *                Pass true to make an action that shows an iframe in a panel
+ *                when clicked.
+ */
+function Action(options) {
+  setProperties(this, options, {
+    id: true,
+    title: !options._isSeparator,
+    iconURL: false,
+    nodeAttributes: false,
+    onCommand: false,
+    onIframeShown: false,
+    onPlacedInPanel: false,
+    onPlacedInUrlbar: false,
+    onShowingInPanel: false,
+    shownInUrlbar: false,
+    subview: false,
+    tooltip: false,
+    urlbarIDOverride: false,
+    wantsIframe: false,
+
+    // private
+
+    // (string, optional)
+    // The ID of another action before which to insert this new action.  Applies
+    // to the page action panel only, not the urlbar.
+    _insertBeforeActionID: false,
+
+    // (bool, optional)
+    // True if this isn't really an action but a separator to be shown in the
+    // page action panel.
+    _isSeparator: false,
+
+    // (bool, optional)
+    // True if the action's urlbar button is defined in markup.  In that case, a
+    // node with the action's urlbar node ID should already exist in the DOM
+    // (either the auto-generated ID or urlbarIDOverride).  That node will be
+    // shown when the action is added to the urlbar and hidden when the action
+    // is removed from the urlbar.
+    _urlbarNodeInMarkup: false,
+  });
+  if (this._subview) {
+    this._subview = new Subview(options.subview);
+  }
+}
+
+Action.prototype = {
+  /**
+   * The action's icon URL (string, nullable)
+   */
+  get iconURL() {
+    return this._iconURL;
+  },
+  set iconURL(url) {
+    this._iconURL = url;
+    PageActions.onActionSetIconURL(this);
+    return this._iconURL;
+  },
+
+  /**
+   * The action's ID (string, nonnull)
+   */
+  get id() {
+    return this._id;
+  },
+
+  /**
+   * Attribute name => value mapping to set on nodes created for this action
+   * (object, nullable)
+   */
+  get nodeAttributes() {
+    return this._nodeAttributes;
+  },
+
+  /**
+   * True if the action is shown in the urlbar (bool, nonnull)
+   */
+  get shownInUrlbar() {
+    return this._shownInUrlbar || false;
+  },
+  set shownInUrlbar(shown) {
+    if (this.shownInUrlbar != shown) {
+      this._shownInUrlbar = shown;
+      PageActions.onActionToggledShownInUrlbar(this);
+    }
+    return this.shownInUrlbar;
+  },
+
+  /**
+   * The action's title (string, nonnull)
+   */
+  get title() {
+    return this._title;
+  },
+  set title(title) {
+    this._title = title || "";
+    PageActions.onActionSetTitle(this);
+    return this._title;
+  },
+
+  /**
+   * The action's tooltip (string, nullable)
+   */
+  get tooltip() {
+    return this._tooltip;
+  },
+
+  /**
+   * Override for the ID of the action's urlbar node (string, nullable)
+   */
+  get urlbarIDOverride() {
+    return this._urlbarIDOverride;
+  },
+
+  /**
+   * True if the action is shown in an iframe (bool, nonnull)
+   */
+  get wantsIframe() {
+    return this._wantsIframe || false;
+  },
+
+  /**
+   * A Subview object if the action wants a subview (Subview, nullable)
+   */
+  get subview() {
+    return this._subview;
+  },
+
+  /**
+   * Call this when the user activates the action.
+   *
+   * @param  event (DOM event, required)
+   *         The triggering event.
+   * @param  buttonNode (DOM node, required)
+   *         The action's panel or urlbar button node that was clicked.
+   */
+  onCommand(event, buttonNode) {
+    if (this._onCommand) {
+      this._onCommand(event, buttonNode);
+    }
+  },
+
+  /**
+   * Call this when the action's iframe is shown.
+   *
+   * @param  iframeNode (DOM node, required)
+   *         The iframe that's being shown.
+   * @param  parentPanelNode (DOM node, required)
+   *         The panel in which the iframe is shown.
+   */
+  onIframeShown(iframeNode, parentPanelNode) {
+    if (this._onIframeShown) {
+      this._onIframeShown(iframeNode, parentPanelNode);
+    }
+  },
+
+  /**
+   * Call this when a DOM node for the action is added to the page action panel.
+   *
+   * @param  buttonNode (DOM node, required)
+   *         The action's panel button node.
+   */
+  onPlacedInPanel(buttonNode) {
+    if (this._onPlacedInPanel) {
+      this._onPlacedInPanel(buttonNode);
+    }
+  },
+
+  /**
+   * Call this when a DOM node for the action is added to the urlbar.
+   *
+   * @param  buttonNode (DOM node, required)
+   *         The action's urlbar button node.
+   */
+  onPlacedInUrlbar(buttonNode) {
+    if (this._onPlacedInUrlbar) {
+      this._onPlacedInUrlbar(buttonNode);
+    }
+  },
+
+  /**
+   * Call this when the action's button is shown in the page action panel.
+   *
+   * @param  buttonNode (DOM node, required)
+   *         The action's panel button node.
+   */
+  onShowingInPanel(buttonNode) {
+    if (this._onShowingInPanel) {
+      this._onShowingInPanel(buttonNode);
+    }
+  },
+
+  /**
+   * Makes PageActions forget about this action and removes its DOM nodes from
+   * all browser windows.  Call this when the user removes your action, like
+   * when your extension is uninstalled.  You probably don't want to call it
+   * simply when your extension is disabled or the app quits, because then
+   * PageActions won't remember it the next time your extension is enabled or
+   * the app starts.
+   */
+  remove() {
+    PageActions.onActionRemoved(this);
+  }
+};
+
+this.PageActions.Action = Action;
+
+
+/**
+ * A Subview represents a PanelUI panelview that your actions can show.
+ *
+ * @param  options (object, required)
+ *         An object with the following properties:
+ *         @param buttons (array, optional)
+ *                An array of buttons to show in the subview.  Each item in the
+ *                array must be an options object suitable for passing to the
+ *                Button constructor.  See the Button constructor for
+ *                information on these objects' properties.
+ *         @param onPlaced (function, optional)
+ *                Called when the subview is added to its parent panel in a
+ *                browser window.  Passed the following arguments:
+ *                * panelViewNode: The panelview node represented by this
+ *                  Subview.
+ *         @param onShowing (function, optional)
+ *                Called when the subview is showing in a browser window.
+ *                Passed the following arguments:
+ *                * panelViewNode: The panelview node represented by this
+ *                  Subview.
+ */
+function Subview(options) {
+  setProperties(this, options, {
+    buttons: false,
+    onPlaced: false,
+    onShowing: false,
+  });
+  this._buttons = (this._buttons || []).map(buttonOptions => {
+    return new Button(buttonOptions);
+  });
+}
+
+Subview.prototype = {
+  /**
+   * The subview's buttons (array of Button objects, nonnull)
+   */
+  get buttons() {
+    return this._buttons;
+  },
+
+  /**
+   * Call this when a DOM node for the subview is added to the DOM.
+   *
+   * @param  panelViewNode (DOM node, required)
+   *         The subview's panelview node.
+   */
+  onPlaced(panelViewNode) {
+    if (this._onPlaced) {
+      this._onPlaced(panelViewNode);
+    }
+  },
+
+  /**
+   * Call this when a DOM node for the subview is showing.
+   *
+   * @param  panelViewNode (DOM node, required)
+   *         The subview's panelview node.
+   */
+  onShowing(panelViewNode) {
+    if (this._onShowing) {
+      this._onShowing(panelViewNode);
+    }
+  }
+};
+
+this.PageActions.Subview = Subview;
+
+
+/**
+ * A button that can be shown in a subview.
+ *
+ * @param  options (object, required)
+ *         An object with the following properties:
+ *         @param id (string, required)
+ *                The button's ID.  This will not become the ID of a DOM node by
+ *                itself, but it will be used to generate DOM node IDs.  But in
+ *                terms of spaces and weird characters and such, do treat this
+ *                like a DOM node ID.
+ *         @param title (string, required)
+ *                The button's title.
+ *         @param disabled (bool, required)
+ *                Pass true to disable the button.
+ *         @param onCommand (function, optional)
+ *                Called when the button is clicked.  Passed the following
+ *                arguments:
+ *                * event: The triggering event.
+ *                * buttonNode: The node that was clicked.
+ *         @param shortcut (string, optional)
+ *                The button's shortcut text.
+ */
+function Button(options) {
+  setProperties(this, options, {
+    id: true,
+    title: true,
+    disabled: false,
+    onCommand: false,
+    shortcut: false,
+  });
+}
+
+Button.prototype = {
+  /**
+   * True if the button is disabled (bool, nonnull)
+   */
+  get disabled() {
+    return this._disabled || false;
+  },
+
+  /**
+   * The button's ID (string, nonnull)
+   */
+  get id() {
+    return this._id;
+  },
+
+  /**
+   * The button's shortcut (string, nullable)
+   */
+  get shortcut() {
+    return this._shortcut;
+  },
+
+  /**
+   * The button's title (string, nonnull)
+   */
+  get title() {
+    return this._title;
+  },
+
+  /**
+   * Call this when the user clicks the button.
+   *
+   * @param  event (DOM event, required)
+   *         The triggering event.
+   * @param  buttonNode (DOM node, required)
+   *         The button's DOM node that was clicked.
+   */
+  onCommand(event, buttonNode) {
+    if (this._onCommand) {
+      this._onCommand(event, buttonNode);
+    }
+  }
+};
+
+this.PageActions.Button = Button;
+
+
+// This is only necessary so that Pocket and the test can specify it for
+// action._insertBeforeActionID.
+this.PageActions.ACTION_ID_BOOKMARK_SEPARATOR = ACTION_ID_BOOKMARK_SEPARATOR;
+
+// This is only necessary so that the test can access it.
+this.PageActions.ACTION_ID_BUILT_IN_SEPARATOR = ACTION_ID_BUILT_IN_SEPARATOR;
+
+
+// Sorted in the order in which they should appear in the page action panel.
+// Does not include the page actions of extensions bundled with the browser.
+// They're added by the relevant extension code.
+var gBuiltInActions = [
+
+  // bookmark
+  {
+    id: "bookmark",
+    urlbarIDOverride: "star-button-box",
+    _urlbarNodeInMarkup: true,
+    title: "",
+    shownInUrlbar: true,
+    nodeAttributes: {
+      observes: "bookmarkThisPageBroadcaster",
+    },
+    onShowingInPanel(buttonNode) {
+      browserPageActions(buttonNode).bookmark.onShowingInPanel(buttonNode);
+    },
+    onCommand(event, buttonNode) {
+      browserPageActions(buttonNode).bookmark.onCommand(event, buttonNode);
+    },
+  },
+
+  // separator
+  {
+    id: ACTION_ID_BOOKMARK_SEPARATOR,
+    _isSeparator: true,
+  },
+
+  // copy URL
+  {
+    id: "copyURL",
+    title: "copyURL-title",
+    onPlacedInPanel(buttonNode) {
+      browserPageActions(buttonNode).copyURL.onPlacedInPanel(buttonNode);
+    },
+    onCommand(event, buttonNode) {
+      browserPageActions(buttonNode).copyURL.onCommand(event, buttonNode);
+    },
+  },
+
+  // email link
+  {
+    id: "emailLink",
+    title: "emailLink-title",
+    onPlacedInPanel(buttonNode) {
+      browserPageActions(buttonNode).emailLink.onPlacedInPanel(buttonNode);
+    },
+    onCommand(event, buttonNode) {
+      browserPageActions(buttonNode).emailLink.onCommand(event, buttonNode);
+    },
+  },
+
+  // send to device
+  {
+    id: "sendToDevice",
+    title: "sendToDevice-title",
+    onPlacedInPanel(buttonNode) {
+      browserPageActions(buttonNode).sendToDevice.onPlacedInPanel(buttonNode);
+    },
+    onShowingInPanel(buttonNode) {
+      browserPageActions(buttonNode).sendToDevice.onShowingInPanel(buttonNode);
+    },
+    subview: {
+      buttons: [
+        {
+          id: "notReady",
+          title: "sendToDevice-notReadyTitle",
+          disabled: true,
+        },
+      ],
+      onPlaced(panelViewNode) {
+        browserPageActions(panelViewNode).sendToDevice
+          .onSubviewPlaced(panelViewNode);
+      },
+      onShowing(panelViewNode) {
+        browserPageActions(panelViewNode).sendToDevice
+          .onShowingSubview(panelViewNode);
+      },
+    },
+  }
+];
+
+
+/**
+ * Gets a BrowserPageActions object in a browser window.
+ *
+ * @param  obj
+ *         Either a DOM node or a browser window.
+ * @return The BrowserPageActions object in the browser window related to the
+ *         given object.
+ */
+function browserPageActions(obj) {
+  if (obj.BrowserPageActions) {
+    return obj.BrowserPageActions;
+  }
+  return obj.ownerGlobal.BrowserPageActions;
+}
+
+/**
+ * A generator function for all open browser windows.
+ */
+function* browserWindows() {
+  let windows = Services.wm.getEnumerator("navigator:browser");
+  while (windows.hasMoreElements()) {
+    yield windows.getNext();
+  }
+}
+
+/**
+ * A simple function that sets properties on a given object while doing basic
+ * required-properties checking.  If a required property isn't specified in the
+ * given options object, or if the options object has properties that aren't in
+ * the given schema, then an error is thrown.
+ *
+ * @param  obj
+ *         The object to set properties on.
+ * @param  options
+ *         An options object supplied by the consumer.
+ * @param  schema
+ *         An object a property for each required and optional property.  The
+ *         keys are property names; the value of a key is a bool that is true if
+ *         the property is required.
+ */
+function setProperties(obj, options, schema) {
+  for (let name in schema) {
+    let required = schema[name];
+    if (required && !(name in options)) {
+      throw new Error(`'${name}' must be specified`);
+    }
+    let nameInObj = "_" + name;
+    if (name[0] == "_") {
+      // The property is "private".  If it's defined in the options, then define
+      // it on obj exactly as it's defined on options.
+      if (name in options) {
+        obj[nameInObj] = options[name];
+      }
+    } else {
+      // The property is "public".  Make sure the property is defined on obj.
+      obj[nameInObj] = options[name] || null;
+    }
+  }
+  for (let name in options) {
+    if (!(name in schema)) {
+      throw new Error(`Unrecognized option '${name}'`);
+    }
+  }
+}
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -144,16 +144,17 @@ EXTRA_JS_MODULES += [
     'DirectoryLinksProvider.jsm',
     'E10SUtils.jsm',
     'ExtensionsUI.jsm',
     'Feeds.jsm',
     'FormSubmitObserver.jsm',
     'FormValidationHandler.jsm',
     'LaterRun.jsm',
     'offlineAppCache.jsm',
+    'PageActions.jsm',
     'PermissionUI.jsm',
     'PluginContent.jsm',
     'ProcessHangMonitor.jsm',
     'ReaderParent.jsm',
     'RecentWindow.jsm',
     'RemotePrompt.jsm',
     'Sanitizer.jsm',
     'SitePermissions.jsm',
--- a/browser/modules/test/browser/browser.ini
+++ b/browser/modules/test/browser/browser.ini
@@ -13,16 +13,17 @@ skip-if = !e10s # Bug 1373549
 [browser_ContentSearch.js]
 support-files =
   contentSearch.js
   contentSearchBadImage.xml
   contentSearchSuggestions.sjs
   contentSearchSuggestions.xml
   !/browser/components/search/test/head.js
   !/browser/components/search/test/testEngine.xml
+[browser_PageActions.js]
 [browser_PermissionUI.js]
 [browser_PermissionUI_prompts.js]
 [browser_ProcessHangNotifications.js]
 skip-if = !e10s
 [browser_SitePermissions.js]
 [browser_SitePermissions_combinations.js]
 [browser_SitePermissions_expiry.js]
 [browser_SitePermissions_tab_urls.js]
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser/browser_PageActions.js
@@ -0,0 +1,703 @@
+"use strict";
+
+// This is a test for PageActions.jsm, specifically the generalized parts that
+// add and remove page actions and toggle them in the urlbar.  This does not
+// test the built-in page actions; browser_page_action_menu.js does that.
+
+// Initialization.  Must run first.
+add_task(async function init() {
+  // The page action urlbar button, and therefore the panel, is only shown when
+  // the current tab is actionable -- i.e., a normal web page.  about:blank is
+  // not, so open a new tab first thing, and close it when this test is done.
+  let tab = await BrowserTestUtils.openNewForegroundTab({
+    gBrowser,
+    url: "http://example.com/",
+  });
+  registerCleanupFunction(async () => {
+    await BrowserTestUtils.removeTab(tab);
+  });
+});
+
+
+// Tests a simple non-built-in action without an iframe or subview.  Also
+// thoroughly checks most of the action's properties, methods, and DOM nodes, so
+// it's not necessary to do that in general in other test tasks.
+add_task(async function simple() {
+  let iconURL = "chrome://browser/skin/email-link.svg";
+  let id = "test-simple";
+  let nodeAttributes = {
+    "test-attr": "test attr value",
+  };
+  let title = "Test simple";
+  let tooltip = "Test simple tooltip";
+
+  let onCommandCallCount = 0;
+  let onPlacedInPanelCallCount = 0;
+  let onPlacedInUrlbarCallCount = 0;
+  let onShowingInPanelCallCount = 0;
+  let onCommandExpectedButtonID;
+
+  let panelButtonID = BrowserPageActions._panelButtonNodeIDForActionID(id);
+  let urlbarButtonID = BrowserPageActions._urlbarButtonNodeIDForActionID(id);
+
+  let initialActions = PageActions.actions;
+
+  let action = PageActions.addAction(new PageActions.Action({
+    iconURL,
+    id,
+    nodeAttributes,
+    title,
+    tooltip,
+    onCommand(event, buttonNode) {
+      onCommandCallCount++;
+      Assert.ok(event, "event should be non-null: " + event);
+      Assert.ok(buttonNode, "buttonNode should be non-null: " + buttonNode);
+      Assert.equal(buttonNode.id, onCommandExpectedButtonID, "buttonNode.id");
+    },
+    onPlacedInPanel(buttonNode) {
+      onPlacedInPanelCallCount++;
+      Assert.ok(buttonNode, "buttonNode should be non-null: " + buttonNode);
+      Assert.equal(buttonNode.id, panelButtonID, "buttonNode.id");
+    },
+    onPlacedInUrlbar(buttonNode) {
+      onPlacedInUrlbarCallCount++;
+      Assert.ok(buttonNode, "buttonNode should be non-null: " + buttonNode);
+      Assert.equal(buttonNode.id, urlbarButtonID, "buttonNode.id");
+    },
+    onShowingInPanel(buttonNode) {
+      onShowingInPanelCallCount++;
+      Assert.ok(buttonNode, "buttonNode should be non-null: " + buttonNode);
+      Assert.equal(buttonNode.id, panelButtonID, "buttonNode.id");
+    },
+  }));
+
+  Assert.equal(action.iconURL, iconURL, "iconURL");
+  Assert.equal(action.id, id, "id");
+  Assert.deepEqual(action.nodeAttributes, nodeAttributes, "nodeAttributes");
+  Assert.equal(action.shownInUrlbar, false, "shownInUrlbar");
+  Assert.equal(action.subview, null, "subview");
+  Assert.equal(action.title, title, "title");
+  Assert.equal(action.tooltip, tooltip, "tooltip");
+  Assert.equal(action.urlbarIDOverride, null, "urlbarIDOverride");
+  Assert.equal(action.wantsIframe, false, "wantsIframe");
+
+  Assert.ok(!("__insertBeforeActionID" in action), "__insertBeforeActionID");
+  Assert.ok(!("__isSeparator" in action), "__isSeparator");
+  Assert.ok(!("__urlbarNodeInMarkup" in action), "__urlbarNodeInMarkup");
+
+  Assert.equal(onPlacedInPanelCallCount, 1,
+               "onPlacedInPanelCallCount should be inc'ed");
+  Assert.equal(onPlacedInUrlbarCallCount, 0,
+               "onPlacedInUrlbarCallCount should remain 0");
+  Assert.equal(onShowingInPanelCallCount, 0,
+               "onShowingInPanelCallCount should remain 0");
+
+  // The separator between the built-in and non-built-in actions should have
+  // been created and included in PageActions.actions, which is why the new
+  // count should be the initial count + 2, not + 1.
+  Assert.equal(PageActions.actions.length, initialActions.length + 2,
+               "PageActions.actions.length should be updated");
+  Assert.deepEqual(PageActions.actions[PageActions.actions.length - 1], action,
+                   "Last page action should be action");
+  Assert.equal(PageActions.actions[PageActions.actions.length - 2].id,
+               PageActions.ACTION_ID_BUILT_IN_SEPARATOR,
+               "2nd-to-last page action should be separator");
+
+  Assert.deepEqual(PageActions.actionForID(action.id), action,
+                   "actionForID should be action");
+
+  // The action's panel button should have been created.
+  let panelButtonNode = document.getElementById(panelButtonID);
+  Assert.notEqual(panelButtonNode, null, "panelButtonNode");
+  Assert.equal(panelButtonNode.getAttribute("label"), action.title, "label");
+  for (let name in action.nodeAttributes) {
+    Assert.ok(panelButtonNode.hasAttribute(name), "Has attribute: " + name);
+    Assert.equal(panelButtonNode.getAttribute(name),
+                 action.nodeAttributes[name],
+                 "Equal attribute: " + name);
+  }
+
+  // The panel button should be the last node in the panel, and its previous
+  // sibling should be the separator between the built-in actions and non-built-
+  // in actions.
+  Assert.equal(panelButtonNode.nextSibling, null, "nextSibling");
+  Assert.notEqual(panelButtonNode.previousSibling, null, "previousSibling");
+  Assert.equal(
+    panelButtonNode.previousSibling.id,
+    BrowserPageActions._panelButtonNodeIDForActionID(
+      PageActions.ACTION_ID_BUILT_IN_SEPARATOR
+    ),
+    "previousSibling.id"
+  );
+
+  // The action's urlbar button should not have been created.
+  let urlbarButtonNode = document.getElementById(urlbarButtonID);
+  Assert.equal(urlbarButtonNode, null, "urlbarButtonNode");
+
+  // Open the panel, click the action's button.
+  await promisePageActionPanelOpen();
+  Assert.equal(onShowingInPanelCallCount, 1,
+               "onShowingInPanelCallCount should be inc'ed");
+  onCommandExpectedButtonID = panelButtonID;
+  EventUtils.synthesizeMouseAtCenter(panelButtonNode, {});
+  await promisePageActionPanelHidden();
+  Assert.equal(onCommandCallCount, 1, "onCommandCallCount should be inc'ed");
+
+  // Show the action's button in the urlbar.
+  action.shownInUrlbar = true;
+  Assert.equal(onPlacedInUrlbarCallCount, 1,
+               "onPlacedInUrlbarCallCount should be inc'ed");
+  urlbarButtonNode = document.getElementById(urlbarButtonID);
+  Assert.notEqual(urlbarButtonNode, null, "urlbarButtonNode");
+  for (let name in action.nodeAttributes) {
+    Assert.ok(urlbarButtonNode.hasAttribute(name), name,
+              "Has attribute: " + name);
+    Assert.equal(urlbarButtonNode.getAttribute(name),
+                 action.nodeAttributes[name],
+                 "Equal attribute: " + name);
+  }
+  onCommandExpectedButtonID = urlbarButtonID;
+  EventUtils.synthesizeMouseAtCenter(urlbarButtonNode, {});
+  Assert.equal(onCommandCallCount, 2, "onCommandCallCount should be inc'ed");
+
+  // Set a new title.
+  let newTitle = title + " new title";
+  action.title = newTitle;
+  Assert.equal(action.title, newTitle, "New title");
+  Assert.equal(panelButtonNode.getAttribute("label"), action.title, "New label");
+
+  // Remove the action.
+  action.remove();
+  panelButtonNode = document.getElementById(panelButtonID);
+  Assert.equal(panelButtonNode, null, "panelButtonNode");
+  urlbarButtonNode = document.getElementById(urlbarButtonID);
+  Assert.equal(urlbarButtonNode, null, "urlbarButtonNode");
+
+  Assert.deepEqual(PageActions.actions, initialActions,
+                   "Actions should go back to initial");
+  Assert.equal(PageActions.actionForID(action.id), null,
+               "actionForID should be null");
+
+  // The separator between the built-in actions and non-built-in actions should
+  // be gone now, too.
+  let separatorNode = document.getElementById(
+    BrowserPageActions._panelButtonNodeIDForActionID(
+      PageActions.ACTION_ID_BUILT_IN_SEPARATOR
+    )
+  );
+  Assert.equal(separatorNode, null, "No separator");
+  Assert.ok(!BrowserPageActions.mainViewBodyNode
+            .lastChild.localName.includes("separator"),
+            "Last child should not be separator");
+});
+
+
+// Tests a non-built-in action with a subview.
+add_task(async function withSubview() {
+  let id = "test-subview";
+
+  let onActionCommandCallCount = 0;
+  let onActionPlacedInPanelCallCount = 0;
+  let onActionPlacedInUrlbarCallCount = 0;
+  let onSubviewPlacedCount = 0;
+  let onSubviewShowingCount = 0;
+  let onButtonCommandCallCount = 0;
+
+  let panelButtonID = BrowserPageActions._panelButtonNodeIDForActionID(id);
+  let urlbarButtonID = BrowserPageActions._urlbarButtonNodeIDForActionID(id);
+
+  let panelViewIDPanel =
+    BrowserPageActions._panelViewNodeIDForActionID(id, false);
+  let panelViewIDUrlbar =
+    BrowserPageActions._panelViewNodeIDForActionID(id, true);
+
+  let onSubviewPlacedExpectedPanelViewID = panelViewIDPanel;
+  let onSubviewShowingExpectedPanelViewID;
+  let onButtonCommandExpectedButtonID;
+
+  let subview = {
+    buttons: [0, 1, 2].map(index => {
+      return {
+        id: "test-subview-button-" + index,
+        title: "Test subview Button " + index,
+      };
+    }),
+    onPlaced(panelViewNode) {
+      onSubviewPlacedCount++;
+      Assert.ok(panelViewNode,
+                "panelViewNode should be non-null: " + panelViewNode);
+      Assert.equal(panelViewNode.id, onSubviewPlacedExpectedPanelViewID,
+                   "panelViewNode.id");
+    },
+    onShowing(panelViewNode) {
+      onSubviewShowingCount++;
+      Assert.ok(panelViewNode,
+                "panelViewNode should be non-null: " + panelViewNode);
+      Assert.equal(panelViewNode.id, onSubviewShowingExpectedPanelViewID,
+                   "panelViewNode.id");
+    },
+  };
+  subview.buttons[0].onCommand = (event, buttonNode) => {
+    onButtonCommandCallCount++;
+    Assert.ok(event, "event should be non-null: " + event);
+    Assert.ok(buttonNode, "buttonNode should be non-null: " + buttonNode);
+    Assert.equal(buttonNode.id, onButtonCommandExpectedButtonID,
+                 "buttonNode.id");
+    for (let node = buttonNode.parentNode; node; node = node.parentNode) {
+      if (node.localName == "panel") {
+        node.hidePopup();
+        break;
+      }
+    }
+  };
+
+  let action = PageActions.addAction(new PageActions.Action({
+    iconURL: "chrome://browser/skin/email-link.svg",
+    id,
+    shownInUrlbar: true,
+    subview,
+    title: "Test subview",
+    onCommand(event, buttonNode) {
+      onActionCommandCallCount++;
+    },
+    onPlacedInPanel(buttonNode) {
+      onActionPlacedInPanelCallCount++;
+      Assert.ok(buttonNode, "buttonNode should be non-null: " + buttonNode);
+      Assert.equal(buttonNode.id, panelButtonID, "buttonNode.id");
+    },
+    onPlacedInUrlbar(buttonNode) {
+      onActionPlacedInUrlbarCallCount++;
+      Assert.ok(buttonNode, "buttonNode should be non-null: " + buttonNode);
+      Assert.equal(buttonNode.id, urlbarButtonID, "buttonNode.id");
+    },
+  }));
+
+  let panelViewButtonIDPanel =
+    BrowserPageActions._panelViewButtonNodeIDForActionID(
+      id, subview.buttons[0].id, false
+    );
+  let panelViewButtonIDUrlbar =
+    BrowserPageActions._panelViewButtonNodeIDForActionID(
+      id, subview.buttons[0].id, true
+    );
+
+  Assert.equal(action.id, id, "id");
+  Assert.notEqual(action.subview, null, "subview");
+  Assert.notEqual(action.subview.buttons, null, "subview.buttons");
+  Assert.equal(action.subview.buttons.length, subview.buttons.length,
+               "subview.buttons.length");
+  for (let i = 0; i < subview.buttons.length; i++) {
+    Assert.equal(action.subview.buttons[i].id, subview.buttons[i].id,
+                 "subview button id for index: " + i);
+    Assert.equal(action.subview.buttons[i].title, subview.buttons[i].title,
+                 "subview button title for index: " + i);
+  }
+
+  Assert.equal(onActionPlacedInPanelCallCount, 1,
+               "onActionPlacedInPanelCallCount should be inc'ed");
+  Assert.equal(onActionPlacedInUrlbarCallCount, 1,
+               "onActionPlacedInUrlbarCallCount should be inc'ed");
+  Assert.equal(onSubviewPlacedCount, 1,
+               "onSubviewPlacedCount should be inc'ed");
+  Assert.equal(onSubviewShowingCount, 0,
+               "onSubviewShowingCount should remain 0");
+
+  // The action's panel button and view (in the main page action panel) should
+  // have been created.
+  let panelButtonNode = document.getElementById(panelButtonID);
+  Assert.notEqual(panelButtonNode, null, "panelButtonNode");
+  let panelViewButtonNodePanel =
+    document.getElementById(panelViewButtonIDPanel);
+  Assert.notEqual(panelViewButtonNodePanel, null, "panelViewButtonNodePanel");
+
+  // The action's urlbar button should have been created.
+  let urlbarButtonNode = document.getElementById(urlbarButtonID);
+  Assert.notEqual(urlbarButtonNode, null, "urlbarButtonNode");
+
+  // Open the panel, click the action's button, click the subview's first
+  // button.
+  await promisePageActionPanelOpen();
+  Assert.equal(onSubviewShowingCount, 0,
+               "onSubviewShowingCount should remain 0");
+  let subviewShownPromise = promisePageActionViewShown();
+  onSubviewShowingExpectedPanelViewID = panelViewIDPanel;
+
+  // synthesizeMouse often cannot seem to click the right node when used on
+  // buttons that show subviews and buttons inside subviews.  That's why we're
+  // using node.click() twice here: the first time to show the subview, the
+  // second time to click a button in the subview.
+//   EventUtils.synthesizeMouseAtCenter(panelButtonNode, {});
+  panelButtonNode.click();
+  await subviewShownPromise;
+  Assert.equal(onActionCommandCallCount, 0,
+               "onActionCommandCallCount should remain 0");
+  Assert.equal(onSubviewShowingCount, 1,
+               "onSubviewShowingCount should be inc'ed");
+  onButtonCommandExpectedButtonID = panelViewButtonIDPanel;
+//   EventUtils.synthesizeMouseAtCenter(panelViewButtonNodePanel, {});
+  panelViewButtonNodePanel.click();
+  await promisePageActionPanelHidden();
+  Assert.equal(onActionCommandCallCount, 0,
+               "onActionCommandCallCount should remain 0");
+  Assert.equal(onButtonCommandCallCount, 1,
+               "onButtonCommandCallCount should be inc'ed");
+
+  // Click the action's urlbar button, which should open the temp panel showing
+  // the subview, and click the subview's first button.
+  onSubviewPlacedExpectedPanelViewID = panelViewIDUrlbar;
+  onSubviewShowingExpectedPanelViewID = panelViewIDUrlbar;
+  EventUtils.synthesizeMouseAtCenter(urlbarButtonNode, {});
+  await promisePanelShown(BrowserPageActions._tempPanelID);
+  Assert.equal(onSubviewPlacedCount, 2,
+               "onSubviewPlacedCount should be inc'ed");
+  Assert.equal(onSubviewShowingCount, 2,
+               "onSubviewShowingCount should be inc'ed");
+  let panelViewButtonNodeUrlbar =
+    document.getElementById(panelViewButtonIDUrlbar);
+  Assert.notEqual(panelViewButtonNodeUrlbar, null, "panelViewButtonNodeUrlbar");
+  onButtonCommandExpectedButtonID = panelViewButtonIDUrlbar;
+  EventUtils.synthesizeMouseAtCenter(panelViewButtonNodeUrlbar, {});
+  await promisePanelHidden(BrowserPageActions._tempPanelID);
+  Assert.equal(onButtonCommandCallCount, 2,
+               "onButtonCommandCallCount should be inc'ed");
+
+  // Remove the action.
+  action.remove();
+  panelButtonNode = document.getElementById(panelButtonID);
+  Assert.equal(panelButtonNode, null, "panelButtonNode");
+  urlbarButtonNode = document.getElementById(urlbarButtonID);
+  Assert.equal(urlbarButtonNode, null, "urlbarButtonNode");
+  let panelViewNodePanel = document.getElementById(panelViewIDPanel);
+  Assert.equal(panelViewNodePanel, null, "panelViewNodePanel");
+  let panelViewNodeUrlbar = document.getElementById(panelViewIDUrlbar);
+  Assert.equal(panelViewNodeUrlbar, null, "panelViewNodeUrlbar");
+});
+
+
+// Tests a non-built-in action with an iframe.
+add_task(async function withIframe() {
+  let id = "test-iframe";
+
+  let onCommandCallCount = 0;
+  let onPlacedInPanelCallCount = 0;
+  let onPlacedInUrlbarCallCount = 0;
+  let onIframeShownCount = 0;
+
+  let panelButtonID = BrowserPageActions._panelButtonNodeIDForActionID(id);
+  let urlbarButtonID = BrowserPageActions._urlbarButtonNodeIDForActionID(id);
+
+  let action = PageActions.addAction(new PageActions.Action({
+    iconURL: "chrome://browser/skin/email-link.svg",
+    id,
+    shownInUrlbar: true,
+    title: "Test iframe",
+    wantsIframe: true,
+    onCommand(event, buttonNode) {
+      onCommandCallCount++;
+    },
+    onIframeShown(iframeNode, panelNode) {
+      onIframeShownCount++;
+      Assert.ok(iframeNode, "iframeNode should be non-null: " + iframeNode);
+      Assert.equal(iframeNode.localName, "iframe", "iframe localName");
+      Assert.ok(panelNode, "panelNode should be non-null: " + panelNode);
+      Assert.equal(panelNode.id, BrowserPageActions._tempPanelID,
+                   "panelNode.id");
+    },
+    onPlacedInPanel(buttonNode) {
+      onPlacedInPanelCallCount++;
+      Assert.ok(buttonNode, "buttonNode should be non-null: " + buttonNode);
+      Assert.equal(buttonNode.id, panelButtonID, "buttonNode.id");
+    },
+    onPlacedInUrlbar(buttonNode) {
+      onPlacedInUrlbarCallCount++;
+      Assert.ok(buttonNode, "buttonNode should be non-null: " + buttonNode);
+      Assert.equal(buttonNode.id, urlbarButtonID, "buttonNode.id");
+    },
+  }));
+
+  Assert.equal(action.id, id, "id");
+  Assert.equal(action.wantsIframe, true, "wantsIframe");
+
+  Assert.equal(onPlacedInPanelCallCount, 1,
+               "onPlacedInPanelCallCount should be inc'ed");
+  Assert.equal(onPlacedInUrlbarCallCount, 1,
+               "onPlacedInUrlbarCallCount should be inc'ed");
+  Assert.equal(onIframeShownCount, 0,
+               "onIframeShownCount should remain 0");
+  Assert.equal(onCommandCallCount, 0,
+               "onCommandCallCount should remain 0");
+
+  // The action's panel button should have been created.
+  let panelButtonNode = document.getElementById(panelButtonID);
+  Assert.notEqual(panelButtonNode, null, "panelButtonNode");
+
+  // The action's urlbar button should have been created.
+  let urlbarButtonNode = document.getElementById(urlbarButtonID);
+  Assert.notEqual(urlbarButtonNode, null, "urlbarButtonNode");
+
+  // Open the panel, click the action's button.
+  await promisePageActionPanelOpen();
+  Assert.equal(onIframeShownCount, 0, "onIframeShownCount should remain 0");
+  EventUtils.synthesizeMouseAtCenter(panelButtonNode, {});
+  await promisePanelShown(BrowserPageActions._tempPanelID);
+  Assert.equal(onCommandCallCount, 0, "onCommandCallCount should remain 0");
+  Assert.equal(onIframeShownCount, 1, "onIframeShownCount should be inc'ed");
+
+  // The temp panel should have opened, anchored to the action's urlbar button.
+  let tempPanel = document.getElementById(BrowserPageActions._tempPanelID);
+  Assert.notEqual(tempPanel, null, "tempPanel");
+  Assert.equal(tempPanel.anchorNode.id, urlbarButtonID,
+               "tempPanel.anchorNode.id");
+  EventUtils.synthesizeMouseAtCenter(urlbarButtonNode, {});
+  await promisePanelHidden(BrowserPageActions._tempPanelID);
+
+  // Click the action's urlbar button.
+  EventUtils.synthesizeMouseAtCenter(urlbarButtonNode, {});
+  await promisePanelShown(BrowserPageActions._tempPanelID);
+  Assert.equal(onCommandCallCount, 0, "onCommandCallCount should remain 0");
+  Assert.equal(onIframeShownCount, 2, "onIframeShownCount should be inc'ed");
+
+  // The temp panel should have opened, again anchored to the action's urlbar
+  // button.
+  tempPanel = document.getElementById(BrowserPageActions._tempPanelID);
+  Assert.notEqual(tempPanel, null, "tempPanel");
+  Assert.equal(tempPanel.anchorNode.id, urlbarButtonID,
+               "tempPanel.anchorNode.id");
+  EventUtils.synthesizeMouseAtCenter(urlbarButtonNode, {});
+  await promisePanelHidden(BrowserPageActions._tempPanelID);
+
+  // Hide the action's button in the urlbar.
+  action.shownInUrlbar = false;
+  urlbarButtonNode = document.getElementById(urlbarButtonID);
+  Assert.equal(urlbarButtonNode, null, "urlbarButtonNode");
+
+  // Open the panel, click the action's button.
+  await promisePageActionPanelOpen();
+  EventUtils.synthesizeMouseAtCenter(panelButtonNode, {});
+  await promisePanelShown(BrowserPageActions._tempPanelID);
+  Assert.equal(onCommandCallCount, 0, "onCommandCallCount should remain 0");
+  Assert.equal(onIframeShownCount, 3, "onIframeShownCount should be inc'ed");
+
+  // The temp panel should have opened, this time anchored to the main page
+  // action button in the urlbar.
+  tempPanel = document.getElementById(BrowserPageActions._tempPanelID);
+  Assert.notEqual(tempPanel, null, "tempPanel");
+  Assert.equal(tempPanel.anchorNode.id, BrowserPageActions.mainButtonNode.id,
+               "tempPanel.anchorNode.id");
+  EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+  await promisePanelHidden(BrowserPageActions._tempPanelID);
+
+  // Remove the action.
+  action.remove();
+  panelButtonNode = document.getElementById(panelButtonID);
+  Assert.equal(panelButtonNode, null, "panelButtonNode");
+  urlbarButtonNode = document.getElementById(urlbarButtonID);
+  Assert.equal(urlbarButtonNode, null, "urlbarButtonNode");
+});
+
+
+// Tests an action with the _insertBeforeActionID option set.
+add_task(async function insertBeforeActionID() {
+  let id = "test-insertBeforeActionID";
+  let panelButtonID = BrowserPageActions._panelButtonNodeIDForActionID(id);
+
+  let initialActions = PageActions.actions;
+  let initialBuiltInActions = PageActions.builtInActions;
+  let initialNonBuiltInActions = PageActions.nonBuiltInActions;
+  let initialBookmarkSeparatorIndex = PageActions.actions.findIndex(a => {
+    return a.id == PageActions.ACTION_ID_BOOKMARK_SEPARATOR;
+  });
+
+  let action = PageActions.addAction(new PageActions.Action({
+    id,
+    title: "Test insertBeforeActionID",
+    _insertBeforeActionID: PageActions.ACTION_ID_BOOKMARK_SEPARATOR,
+  }));
+
+  Assert.equal(action.id, id, "id");
+  Assert.ok("__insertBeforeActionID" in action, "__insertBeforeActionID");
+  Assert.equal(action.__insertBeforeActionID,
+               PageActions.ACTION_ID_BOOKMARK_SEPARATOR,
+               "action.__insertBeforeActionID");
+
+  Assert.equal(PageActions.actions.length,
+               initialActions.length + 1,
+               "PageActions.actions.length should be updated");
+  Assert.equal(PageActions.builtInActions.length,
+               initialBuiltInActions.length + 1,
+               "PageActions.builtInActions.length should be updated");
+  Assert.equal(PageActions.nonBuiltInActions.length,
+               initialNonBuiltInActions.length,
+               "PageActions.nonBuiltInActions.length should be updated");
+
+  let actionIndex = PageActions.actions.findIndex(a => a.id == id);
+  Assert.equal(initialBookmarkSeparatorIndex, actionIndex,
+               "initialBookmarkSeparatorIndex");
+  let newBookmarkSeparatorIndex = PageActions.actions.findIndex(a => {
+    return a.id == PageActions.ACTION_ID_BOOKMARK_SEPARATOR;
+  });
+  Assert.equal(newBookmarkSeparatorIndex, initialBookmarkSeparatorIndex + 1,
+               "newBookmarkSeparatorIndex");
+
+  // The action's panel button should have been created.
+  let panelButtonNode = document.getElementById(panelButtonID);
+  Assert.notEqual(panelButtonNode, null, "panelButtonNode");
+
+  // The button's next sibling should be the bookmark separator.
+  Assert.notEqual(panelButtonNode.nextSibling, null,
+                  "panelButtonNode.nextSibling");
+  Assert.equal(
+    panelButtonNode.nextSibling.id,
+    BrowserPageActions._panelButtonNodeIDForActionID(
+      PageActions.ACTION_ID_BOOKMARK_SEPARATOR
+    ),
+    "panelButtonNode.nextSibling.id"
+  );
+
+  // The separator between the built-in and non-built-in actions should not have
+  // been created.
+  Assert.equal(
+    document.getElementById(
+      BrowserPageActions._panelButtonNodeIDForActionID(
+        PageActions.ACTION_ID_BUILT_IN_SEPARATOR
+      )
+    ),
+    null,
+    "Separator should be gone"
+  );
+
+  action.remove();
+});
+
+
+// Tests that the ordering of multiple non-built-in actions is alphabetical.
+add_task(async function multipleNonBuiltInOrdering() {
+  let idPrefix = "test-multipleNonBuiltInOrdering-";
+  let titlePrefix = "Test multipleNonBuiltInOrdering ";
+
+  let initialActions = PageActions.actions;
+  let initialBuiltInActions = PageActions.builtInActions;
+  let initialNonBuiltInActions = PageActions.nonBuiltInActions;
+
+  // Create some actions in an out-of-order order.
+  let actions = [2, 1, 4, 3].map(index => {
+    return PageActions.addAction(new PageActions.Action({
+      id: idPrefix + index,
+      title: titlePrefix + index,
+    }));
+  });
+
+  // + 1 for the separator between built-in and non-built-in actions.
+  Assert.equal(PageActions.actions.length,
+               initialActions.length + actions.length + 1,
+               "PageActions.actions.length should be updated");
+
+  Assert.equal(PageActions.builtInActions.length,
+               initialBuiltInActions.length,
+               "PageActions.builtInActions.length should be same");
+  Assert.equal(PageActions.nonBuiltInActions.length,
+               initialNonBuiltInActions.length + actions.length,
+               "PageActions.nonBuiltInActions.length should be updated");
+
+  // Look at the final actions.length actions in PageActions.actions, from first
+  // to last.
+  for (let i = 0; i < actions.length; i++) {
+    let expectedIndex = i + 1;
+    let actualAction = PageActions.nonBuiltInActions[i];
+    Assert.equal(actualAction.id, idPrefix + expectedIndex,
+                 "actualAction.id for index: " + i);
+  }
+
+  // Check the button nodes in the panel.
+  let expectedIndex = 1;
+  let buttonNode = document.getElementById(
+    BrowserPageActions._panelButtonNodeIDForActionID(idPrefix + expectedIndex)
+  );
+  Assert.notEqual(buttonNode, null, "buttonNode");
+  Assert.notEqual(buttonNode.previousSibling, null,
+                  "buttonNode.previousSibling");
+  Assert.equal(
+    buttonNode.previousSibling.id,
+    BrowserPageActions._panelButtonNodeIDForActionID(
+      PageActions.ACTION_ID_BUILT_IN_SEPARATOR
+    ),
+    "buttonNode.previousSibling.id"
+  );
+  for (let i = 0; i < actions.length; i++) {
+    Assert.notEqual(buttonNode, null, "buttonNode at index: " + i);
+    Assert.equal(
+      buttonNode.id,
+      BrowserPageActions._panelButtonNodeIDForActionID(idPrefix + expectedIndex),
+      "buttonNode.id at index: " + i
+    );
+    buttonNode = buttonNode.nextSibling;
+    expectedIndex++;
+  }
+  Assert.equal(buttonNode, null, "Nothing should come after the last button");
+
+  for (let action of actions) {
+    action.remove();
+  }
+
+  // The separator between the built-in and non-built-in actions should be gone.
+  Assert.equal(
+    document.getElementById(
+      BrowserPageActions._panelButtonNodeIDForActionID(
+        PageActions.ACTION_ID_BUILT_IN_SEPARATOR
+      )
+    ),
+    null,
+    "Separator should be gone"
+  );
+});
+
+
+function promisePageActionPanelOpen() {
+  let button = document.getElementById("pageActionButton");
+  let shownPromise = promisePageActionPanelShown();
+  EventUtils.synthesizeMouseAtCenter(button, {});
+  return shownPromise;
+}
+
+function promisePageActionPanelShown() {
+  return promisePanelShown(BrowserPageActions.panelNode);
+}
+
+function promisePageActionPanelHidden() {
+  return promisePanelHidden(BrowserPageActions.panelNode);
+}
+
+function promisePanelShown(panelIDOrNode) {
+  return promisePanelEvent(panelIDOrNode, "popupshown");
+}
+
+function promisePanelHidden(panelIDOrNode) {
+  return promisePanelEvent(panelIDOrNode, "popuphidden");
+}
+
+function promisePanelEvent(panelIDOrNode, eventType) {
+  return new Promise(resolve => {
+    let panel = typeof(panelIDOrNode) != "string" ? panelIDOrNode :
+                document.getElementById(panelIDOrNode);
+    if (!panel ||
+        (eventType == "popupshowing" && panel.state == "open") ||
+        (eventType == "popuphidden" && panel.state == "closed")) {
+      executeSoon(resolve);
+      return;
+    }
+    panel.addEventListener(eventType, () => {
+      executeSoon(resolve);
+    }, { once: true });
+  });
+}
+
+function promisePageActionViewShown() {
+  return new Promise(resolve => {
+    BrowserPageActions.panelNode.addEventListener("ViewShown", (event) => {
+      let target = event.originalTarget;
+      window.setTimeout(() => {
+        resolve(target);
+      }, 5000);
+    }, { once: true });
+  });
+}
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -512,27 +512,26 @@ html|*.addon-webext-perm-list {
 }
 
 .addon-addon-icon,
 .addon-toolbar-icon {
   width: 14px;
   height: 14px;
   vertical-align: bottom;
   margin-bottom: 1px;
+  -moz-context-properties: fill;
+  fill: currentColor;
 }
 
 .addon-addon-icon {
-  list-style-image: url("chrome://browser/skin/menuPanel.svg");
-  -moz-image-region: rect(0px, 288px, 32px, 256px);
+  list-style-image: url("chrome://browser/skin/addons.svg");
 }
 
 .addon-toolbar-icon {
   list-style-image: url("chrome://browser/skin/menu.svg");
-  -moz-context-properties: fill;
-  fill: var(--toolbarbutton-icon-fill);
 }
 
 /* Notification icon box */
 
 .notification-anchor-icon:-moz-focusring {
   outline: 1px dotted -moz-DialogText;
 }
 
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -1846,27 +1846,26 @@ html|*.addon-webext-perm-list {
 }
 
 .addon-addon-icon,
 .addon-toolbar-icon {
   width: 14px;
   height: 14px;
   vertical-align: bottom;
   margin-bottom: 1px;
+  -moz-context-properties: fill;
+  fill: currentColor;
 }
 
 .addon-addon-icon {
-  list-style-image: url("chrome://browser/skin/menuPanel.svg");
-  -moz-image-region: rect(0px, 288px, 32px, 256px);
+  list-style-image: url("chrome://browser/skin/addons.svg");
 }
 
 .addon-toolbar-icon {
   list-style-image: url("chrome://browser/skin/menu.svg");
-  -moz-context-properties: fill;
-  fill: var(--toolbarbutton-icon-fill);
 }
 
 /* Status panel */
 
 .statuspanel-label {
   margin: 0;
   padding: 2px 4px;
   background-color: #f9f9fa;
--- a/browser/themes/shared/icons/device-desktop.svg
+++ b/browser/themes/shared/icons/device-desktop.svg
@@ -1,6 +1,6 @@
 <!-- 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/. -->
 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
-  <path fill="context-fill" d="M0 12h16a1.959 1.959 0 0 1-2 2H2a1.959 1.959 0 0 1-2-2zM13.107 2H2.894A1.894 1.894 0 0 0 1 3.894V11h14V3.893A1.893 1.893 0 0 0 13.107 2zM14 10H2V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1z"/>
+  <path fill-opacity="context-fill-opacity" fill="context-fill" d="M0 12h16a1.959 1.959 0 0 1-2 2H2a1.959 1.959 0 0 1-2-2zM13.107 2H2.894A1.894 1.894 0 0 0 1 3.894V11h14V3.893A1.893 1.893 0 0 0 13.107 2zM14 10H2V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1z"/>
 </svg>
--- a/browser/themes/shared/icons/device-mobile.svg
+++ b/browser/themes/shared/icons/device-mobile.svg
@@ -1,6 +1,6 @@
 <!-- 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/. -->
 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
-  <path fill="context-fill" d="M10 1H6a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2zm1 11.5a.5.5 0 0 1-.5.5h-5a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 .5.5z"/>
+  <path fill-opacity="context-fill-opacity" fill="context-fill" d="M10 1H6a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2zm1 11.5a.5.5 0 0 1-.5.5h-5a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 .5.5z"/>
 </svg>
--- a/browser/themes/shared/icons/email-link.svg
+++ b/browser/themes/shared/icons/email-link.svg
@@ -1,7 +1,7 @@
 <!-- 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/. -->
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
-  <path fill="context-fill" d="M13 2H3a3.013 3.013 0 0 0-3 3v6a3.013 3.013 0 0 0 3 3h10a3.013 3.013 0 0 0 3-3V5a3.013 3.013 0 0 0-3-3zm1 9a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1z"/>
-  <path fill="context-fill" d="M8 9a.5.5 0 0 1-.294-.1l-5.5-4a.5.5 0 1 1 .588-.8L8 7.882 13.207 4.1a.5.5 0 0 1 .588.809l-5.5 4A.5.5 0 0 1 8 9z"/>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+  <path fill-opacity="context-fill-opacity" fill="context-fill" d="M13 2H3a3.013 3.013 0 0 0-3 3v6a3.013 3.013 0 0 0 3 3h10a3.013 3.013 0 0 0 3-3V5a3.013 3.013 0 0 0-3-3zm1 9a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1z"/>
+  <path fill-opacity="context-fill-opacity" fill="context-fill" d="M8 9a.5.5 0 0 1-.294-.1l-5.5-4a.5.5 0 1 1 .588-.8L8 7.882 13.207 4.1a.5.5 0 0 1 .588.809l-5.5 4A.5.5 0 0 1 8 9z"/>
 </svg>
--- a/browser/themes/shared/icons/link.svg
+++ b/browser/themes/shared/icons/link.svg
@@ -1,8 +1,8 @@
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
-  <rect x="7" y="3.286" width="2" height="9.429" rx="1" ry="1" transform="rotate(-45 8 8)"/>
-  <path fill="context-fill" d="M2.354 4.522L4.485 2.39a.5.5 0 0 1 .711 0l3.19 3.19.014-.015a2 2 0 0 0 0-2.821L6.272.616a2 2 0 0 0-2.821 0L.616 3.451a2 2 0 0 0 0 2.821L2.744 8.4a1.993 1.993 0 0 0 2.8.02l-3.19-3.186a.5.5 0 0 1 0-.712z"/>
-  <path fill="context-fill" d="M15.416 9.759L13.287 7.63a2 2 0 0 0-2.821 0l-.015.015 3.189 3.189a.5.5 0 0 1 0 .711l-2.132 2.132a.5.5 0 0 1-.711 0L7.61 10.49a1.993 1.993 0 0 0 .02 2.8l2.128 2.128a2 2 0 0 0 2.821 0l2.835-2.835a2 2 0 0 0 .002-2.824z"/>
+  <rect fill-opacity="context-fill-opacity" fill="context-fill" x="7" y="3.286" width="2" height="9.429" rx="1" ry="1" transform="rotate(-45 8 8)"/>
+  <path fill-opacity="context-fill-opacity" fill="context-fill" d="M2.354 4.522L4.485 2.39a.5.5 0 0 1 .711 0l3.19 3.19.014-.015a2 2 0 0 0 0-2.821L6.272.616a2 2 0 0 0-2.821 0L.616 3.451a2 2 0 0 0 0 2.821L2.744 8.4a1.993 1.993 0 0 0 2.8.02l-3.19-3.186a.5.5 0 0 1 0-.712z"/>
+  <path fill-opacity="context-fill-opacity" fill="context-fill" d="M15.416 9.759L13.287 7.63a2 2 0 0 0-2.821 0l-.015.015 3.189 3.189a.5.5 0 0 1 0 .711l-2.132 2.132a.5.5 0 0 1-.711 0L7.61 10.49a1.993 1.993 0 0 0 .02 2.8l2.128 2.128a2 2 0 0 0 2.821 0l2.835-2.835a2 2 0 0 0 .002-2.824z"/>
 </svg>
--- a/browser/themes/shared/icons/page-action.svg
+++ b/browser/themes/shared/icons/page-action.svg
@@ -1,6 +1,6 @@
 <!-- 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/. -->
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
   <path fill-opacity="context-fill-opacity" fill="context-fill" d="M2 6a2 2 0 1 0 2 2 2 2 0 0 0-2-2zm6 0a2 2 0 1 0 2 2 2 2 0 0 0-2-2zm6 0a2 2 0 1 0 2 2 2 2 0 0 0-2-2z"/>
 </svg>
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -70,17 +70,19 @@
   skin/classic/browser/connection-mixed-active-loaded.svg      (../shared/identity-block/connection-mixed-active-loaded.svg)
   skin/classic/browser/identity-icon.svg                       (../shared/identity-block/identity-icon.svg)
   skin/classic/browser/identity-icon-notice.svg                (../shared/identity-block/identity-icon-notice.svg)
 #ifndef MOZ_PHOTON_THEME
   skin/classic/browser/identity-icon-hover.svg                 (../shared/identity-block/identity-icon-hover.svg)
   skin/classic/browser/identity-icon-notice-hover.svg          (../shared/identity-block/identity-icon-notice-hover.svg)
 #endif
   skin/classic/browser/info.svg                                (../shared/info.svg)
+#ifndef MOZ_PHOTON_THEME
 * skin/classic/browser/menuPanel.svg                           (../shared/menuPanel.svg)
+#endif
 * skin/classic/browser/menuPanel-small.svg                     (../shared/menuPanel-small.svg)
   skin/classic/browser/notification-icons.svg                  (../shared/notification-icons.svg)
   skin/classic/browser/tracking-protection-16.svg              (../shared/identity-block/tracking-protection-16.svg)
   skin/classic/browser/newtab/close.png                        (../shared/newtab/close.png)
   skin/classic/browser/newtab/controls.svg                     (../shared/newtab/controls.svg)
   skin/classic/browser/panel-icon-arrow-left.svg               (../shared/panel-icon-arrow-left.svg)
   skin/classic/browser/panel-icon-arrow-right.svg              (../shared/panel-icon-arrow-right.svg)
   skin/classic/browser/panel-icon-cancel.svg                   (../shared/panel-icon-cancel.svg)
--- a/browser/themes/shared/toolbarbutton-icons.inc.css
+++ b/browser/themes/shared/toolbarbutton-icons.inc.css
@@ -398,16 +398,20 @@ toolbar:not([brighttext]) #bookmarks-men
   animation-name: overflow-animation-rtl;
 }
 
 #nav-bar-overflow-button[animate][fade] > .toolbarbutton-animatable-box > .toolbarbutton-animatable-image {
   animation-name: overflow-fade;
   animation-timing-function: ease-out;
   animation-duration: 730ms;
 }
+
+#nav-bar-overflow-button[animate][fade]:-moz-locale-dir(rtl) > .toolbarbutton-animatable-box > .toolbarbutton-animatable-image {
+  transform: scaleX(-1);
+}
 %endif
 
 #email-link-button@attributeSelectorForToolbar@ {
   list-style-image: url("chrome://browser/skin/mail.svg");
 }
 
 #sidebar-button@attributeSelectorForToolbar@ {
   list-style-image: url("chrome://browser/skin/sidebars-right.svg");
--- a/browser/themes/shared/urlbar-searchbar.inc.css
+++ b/browser/themes/shared/urlbar-searchbar.inc.css
@@ -60,37 +60,83 @@
   margin: 0 -6px;
   position: relative;
   border: none;
   background: transparent;
   -moz-appearance: none;
 }
 
 %ifdef MOZ_PHOTON_THEME
-/* Page action button */
-#urlbar-page-action-button {
-  -moz-appearance: none;
-  border-style: none;
-  list-style-image: url("chrome://browser/skin/page-action.svg");
-  margin: 0;
-  padding: 0 6px;
+/* Page action panel */
+.pageAction-panel-button > .toolbarbutton-icon {
+  width: 16px;
+  height: 16px;
+}
+
+#pageAction-panel-bookmark,
+#star-button {
+  list-style-image: url("chrome://browser/skin/bookmark-hollow.svg");
+}
+#pageAction-panel-bookmark[starred],
+#star-button[starred] {
+  list-style-image: url("chrome://browser/skin/bookmark.svg");
+}
+#star-button[starred] {
+  fill-opacity: 1;
+  fill: var(--toolbarbutton-icon-fill-attention);
+}
+
+#pageAction-panel-copyURL,
+#pageAction-urlbar-copyURL {
+  list-style-image: url("chrome://browser/skin/link.svg");
+}
+
+#pageAction-panel-emailLink,
+#pageAction-urlbar-emailLink {
+  list-style-image: url("chrome://browser/skin/email-link.svg");
 }
 
-#urlbar-page-action-button > .toolbarbutton-icon {
-  width: 16px;
+#pageAction-panel-sendToDevice,
+#pageAction-urlbar-sendToDevice {
+  list-style-image: url("chrome://browser/skin/device-mobile.svg");
+}
+
+.pageAction-sendToDevice-device[clientType=mobile] {
+  list-style-image: url("chrome://browser/skin/device-mobile.svg");
+}
+
+.pageAction-sendToDevice-device[clientType=desktop] {
+  list-style-image: url("chrome://browser/skin/device-desktop.svg");
 }
 
-#urlbar-page-action-button,
+#pageAction-panel-sendToDevice-fxa,
+#pageAction-urlbar-sendToDevice-fxa {
+  list-style-image: url("chrome://browser/skin/sync.svg");
+}
+
+/* Page action urlbar buttons */
+#urlbar-icons {
+  /* Add more space between the last icon and the urlbar's edge. */
+  margin-inline-end: 3px;
+}
+
 .urlbar-icon {
+  width: 22px;
+  height: 16px;
+  margin-inline-start: 6px;
   -moz-context-properties: fill, fill-opacity;
   fill: currentColor;
   fill-opacity: 0.6;
   color: inherit;
 }
 
+#pageActionButton {
+  list-style-image: url("chrome://browser/skin/page-action.svg");
+}
+
 %ifdef MOZ_PHOTON_ANIMATIONS
 @keyframes bookmark-animation {
   from {
     transform: translateX(0);
   }
   to {
     transform: translateX(-627px);
   }
@@ -103,29 +149,17 @@
   to {
     transform: scaleX(-1) translateX(-627px);
   }
 }
 
 #star-button-box[animationsenabled] {
   position: relative;
 }
-%endif /* MOZ_PHOTON_ANIMATIONS */
 
-#star-button {
-  list-style-image: url("chrome://browser/skin/bookmark-hollow.svg");
-}
-
-#star-button[starred] {
-  list-style-image: url("chrome://browser/skin/bookmark.svg");
-  fill-opacity: 1;
-  fill: var(--toolbarbutton-icon-fill-attention);
-}
-
-%ifdef MOZ_PHOTON_ANIMATIONS
 /* Preload the bookmark animations to prevent a flicker during the first playing
    of the animations. */
 #star-button[preloadanimations] + #star-button-animatable-box > #star-button-animatable-image {
   background-image: url("chrome://browser/skin/bookmark-animation.svg"),
                     url("chrome://browser/skin/library-bookmark-animation.svg");
   background-size: 0, 0;
 }
 
@@ -134,18 +168,19 @@
   position: relative;
 }
 
 #star-button-box[animationsenabled] > #star-button[starred][animate] + #star-button-animatable-box {
   position: absolute;
   overflow: hidden;
   top: calc(50% - 16.5px); /* 16.5px is half the height of the sprite */
   /* .urlbar-icon has width 22px. Each frame is 33px wide. Set margin-inline-start
-     to be half the difference. */
-  margin-inline-start: -5.5px;
+     to be half the difference, -5.5px, plus the 6px margin-inline-start of
+     .urlbar-icon, 6px. */
+  margin-inline-start: 0.5px;
   /* Set the height to equal the height of each frame of the SVG. Must use
      min- and max- width and height due to bug 1379332. */
   min-width: 33px;
   max-width: 33px;
   min-height: 33px;
   max-height: 33px;
 }
 
@@ -161,49 +196,16 @@
   animation-duration: 304ms;
   width: 660px;
 }
 
 #star-button-box[animationsenabled] > #star-button[starred][animate]:-moz-locale-dir(rtl) + #star-button-animatable-box > #star-button-animatable-image {
   animation-name: bookmark-animation-rtl;
 }
 %endif /* MOZ_PHOTON_ANIMATIONS */
-
-/* Page action popup */
-#page-action-bookmark-button {
-  list-style-image: url("chrome://browser/skin/bookmark-hollow.svg");
-}
-
-#page-action-bookmark-button[starred] {
-  list-style-image: url("chrome://browser/skin/bookmark.svg");
-}
-
-#page-action-copy-url-button {
-  list-style-image: url("chrome://browser/skin/link.svg");
-}
-
-#page-action-email-link-button {
-  list-style-image: url("chrome://browser/skin/email-link.svg");
-}
-
-#page-action-send-to-device-button {
-  list-style-image: url("chrome://browser/skin/device-mobile.svg");
-}
-
-.page-action-sendToDevice-device[clientType=mobile] {
-  list-style-image: url("chrome://browser/skin/device-mobile.svg");
-}
-
-.page-action-sendToDevice-device[clientType=desktop] {
-  list-style-image: url("chrome://browser/skin/device-desktop.svg");
-}
-
-#page-action-sendToDevice-fxa-button {
-  list-style-image: url("chrome://browser/skin/sync.svg");
-}
 %endif /* MOZ_PHOTON_THEME */
 
 /* Zoom button */
 #urlbar-zoom-button {
   margin: 0 3px;
   font-size: .8em;
   padding: 0 8px;
   border-radius: 1em;
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -1557,27 +1557,26 @@ html|*.addon-webext-perm-list {
 }
 
 .addon-addon-icon,
 .addon-toolbar-icon {
   width: 14px;
   height: 14px;
   vertical-align: bottom;
   margin-bottom: 1px;
+  -moz-context-properties: fill;
+  fill: currentColor;
 }
 
 .addon-addon-icon {
-  list-style-image: url("chrome://browser/skin/menuPanel.svg");
-  -moz-image-region: rect(0px, 288px, 32px, 256px);
+  list-style-image: url("chrome://browser/skin/addons.svg");
 }
 
 .addon-toolbar-icon {
   list-style-image: url("chrome://browser/skin/menu.svg");
-  -moz-context-properties: fill;
-  fill: var(--toolbarbutton-icon-fill);
 }
 
 /* Notification icon box */
 
 .notification-anchor-icon:-moz-focusring {
   outline: 1px dotted -moz-DialogText;
 }
 
--- a/devtools/client/devtools-startup.js
+++ b/devtools/client/devtools-startup.js
@@ -245,17 +245,17 @@ DevToolsStartup.prototype = {
       viewId: "PanelUI-developer",
       shortcutId: "key_toggleToolbox",
       tooltiptext: "developer-button.tooltiptext2",
       defaultArea: AppConstants.MOZ_DEV_EDITION ?
                      CustomizableUI.AREA_NAVBAR :
                      CustomizableUI.AREA_PANEL,
       onViewShowing: (event) => {
         // Ensure creating the menuitems in the system menu before trying to copy them.
-        this.initDevTools();
+        this.initDevTools("HamburgerMenu");
 
         // Populate the subview with whatever menuitems are in the developer
         // menu. We skip menu elements, because the menu panel has no way
         // of dealing with those right now.
         let doc = event.target.ownerDocument;
 
         let menu = doc.getElementById("menuWebDeveloperPopup");
 
@@ -295,17 +295,19 @@ DevToolsStartup.prototype = {
 
   /*
    * We listen to the "Web Developer" system menu, which is under "Tools" main item.
    * This menu item is hardcoded empty in Firefox UI. We listen for its opening to
    * populate it lazily. Loading main DevTools module is going to populate it.
    */
   hookWebDeveloperMenu(window) {
     let menu = window.document.getElementById("webDeveloperMenu");
-    menu.addEventListener("popupshowing", () => this.initDevTools(), { once: true });
+    menu.addEventListener("popupshowing", () => {
+      this.initDevTools("SystemMenu");
+    }, { once: true });
   },
 
   hookKeyShortcuts(window) {
     let doc = window.document;
     let keyset = doc.createElement("keyset");
     keyset.setAttribute("id", "devtoolsKeyset");
 
     for (let key of KeyShortcuts) {
@@ -316,17 +318,17 @@ DevToolsStartup.prototype = {
     // Appending a <key> element is not always enough. The <keyset> needs
     // to be detached and reattached to make sure the <key> is taken into
     // account (see bug 832984).
     let mainKeyset = doc.getElementById("mainKeyset");
     mainKeyset.parentNode.insertBefore(keyset, mainKeyset);
   },
 
   onKey(window, key) {
-    let require = this.initDevTools();
+    let require = this.initDevTools("KeyShortcut");
     let { gDevToolsBrowser } = require("devtools/client/framework/devtools-browser");
     gDevToolsBrowser.onKeyShortcut(window, key);
   },
 
   // Create a <xul:key> DOM Element
   createKey(doc, { id, toolId, shortcut, modifiers: mod }, oncommand) {
     let k = doc.createElement("key");
     k.id = "key_" + (id || toolId);
@@ -349,29 +351,40 @@ DevToolsStartup.prototype = {
   },
 
   /**
    * Boolean flag to check if DevTools have been already initialized or not.
    * By initialized, we mean that its main modules are loaded.
    */
   initialized: false,
 
-  initDevTools: function () {
+  initDevTools: function (reason) {
+    if (!this.initialized) {
+      // Only save the first call for each firefox run as next call
+      // won't necessarely start the tool. For example key shortcuts may
+      // only change the currently selected tool.
+      try {
+        Services.telemetry.getHistogramById("DEVTOOLS_ENTRY_POINT")
+                          .add(reason);
+      } catch (e) {
+        dump("DevTools telemetry entry point failed: " + e + "\n");
+      }
+    }
     this.initialized = true;
     let { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
     // Ensure loading main devtools module that hooks up into browser UI
     // and initialize all devtools machinery.
     require("devtools/client/framework/devtools-browser");
     return require;
   },
 
   handleConsoleFlag: function (cmdLine) {
     let window = Services.wm.getMostRecentWindow("devtools:webconsole");
     if (!window) {
-      this.initDevTools();
+      this.initDevTools("CommandLine");
 
       let { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
       let hudservice = require("devtools/client/webconsole/hudservice");
       let { console } = Cu.import("resource://gre/modules/Console.jsm", {});
       hudservice.toggleBrowserConsole().catch(console.error);
     } else {
       // the Browser Console was already open
       window.focus();
@@ -379,17 +392,17 @@ DevToolsStartup.prototype = {
 
     if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) {
       cmdLine.preventDefault = true;
     }
   },
 
   // Open the toolbox on the selected tab once the browser starts up.
   handleDevToolsFlag: function (window) {
-    const require = this.initDevTools();
+    const require = this.initDevTools("CommandLine");
     const {gDevTools} = require("devtools/client/framework/devtools");
     const {TargetFactory} = require("devtools/client/framework/target");
     let target = TargetFactory.forTab(window.gBrowser.selectedTab);
     gDevTools.showToolbox(target);
   },
 
   _isRemoteDebuggingEnabled() {
     let remoteDebuggingEnabled = false;
--- a/devtools/client/framework/devtools-browser.js
+++ b/devtools/client/framework/devtools-browser.js
@@ -298,17 +298,17 @@ var gDevToolsBrowser = exports.gDevTools
     }
     // Otherwise implement all other key shortcuts individually here
     switch (key.id) {
       case "toggleToolbox":
       case "toggleToolboxF12":
         gDevToolsBrowser.toggleToolboxCommand(window.gBrowser);
         break;
       case "toggleToolbar":
-        window.DeveloperToolbar.focusToggle();
+        gDevToolsBrowser.getDeveloperToolbar(window).focusToggle();
         break;
       case "webide":
         gDevToolsBrowser.openWebIDE();
         break;
       case "browserToolbox":
         BrowserToolboxProcess.init();
         break;
       case "browserConsole":
--- a/devtools/client/framework/target.js
+++ b/devtools/client/framework/target.js
@@ -707,16 +707,36 @@ TabTarget.prototype = {
     this._url = null;
     this.threadActor = null;
   },
 
   toString: function () {
     let id = this._tab ? this._tab : (this._form && this._form.actor);
     return `TabTarget:${id}`;
   },
+
+  /**
+   * Log an error of some kind to the tab's console.
+   *
+   * @param {String} text
+   *                 The text to log.
+   * @param {String} category
+   *                 The category of the message.  @see nsIScriptError.
+   */
+  logErrorInPage: function (text, category) {
+    if (this.activeTab && this.activeTab.traits.logErrorInPage) {
+      let packet = {
+        to: this.form.actor,
+        type: "logErrorInPage",
+        text,
+        category,
+      };
+      this.client.request(packet);
+    }
+  },
 };
 
 /**
  * WebProgressListener for TabTarget.
  *
  * @param object target
  *        The TabTarget instance to work with.
  */
@@ -857,10 +877,14 @@ WorkerTarget.prototype = {
   },
 
   getTrait: function () {
     return undefined;
   },
 
   makeRemote: function () {
     return Promise.resolve();
-  }
+  },
+
+  logErrorInPage: function () {
+    // No-op.  See bug 1368680.
+  },
 };
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/inspector.js
@@ -1044,23 +1044,19 @@ Inspector.prototype = {
     return this._panelDestroyer;
   },
 
   /**
    * Returns the clipboard content if it is appropriate for pasting
    * into the current node's outer HTML, otherwise returns null.
    */
   _getClipboardContentForPaste: function () {
-    let flavors = clipboardHelper.getCurrentFlavors();
-    if (flavors.indexOf("text") != -1 ||
-        (flavors.indexOf("html") != -1 && flavors.indexOf("image") == -1)) {
-      let content = clipboardHelper.getData();
-      if (content && content.trim().length > 0) {
-        return content;
-      }
+    let content = clipboardHelper.getText();
+    if (content && content.trim().length > 0) {
+      return content;
     }
     return null;
   },
 
   _onContextMenu: function (e) {
     e.preventDefault();
     this._openMenu({
       screenX: e.screenX,
--- a/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
+++ b/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
@@ -41,28 +41,32 @@ NewConsoleOutputWrapper.prototype = {
     };
     // Focus the input line whenever the output area is clicked.
     this.parentNode.addEventListener("click", (event) => {
       // Do not focus on middle/right-click or 2+ clicks.
       if (event.detail !== 1 || event.button !== 0) {
         return;
       }
 
+      // Do not focus if a link was clicked
+      if (event.originalTarget.closest("a")) {
+        return;
+      }
+
+      // Do not focus if something other than the output region was clicked
+      if (!event.originalTarget.closest(".webconsole-output")) {
+        return;
+      }
+
       // Do not focus if something is selected
       let selection = this.document.defaultView.getSelection();
       if (selection && !selection.isCollapsed) {
         return;
       }
 
-      // Do not focus if a link was clicked
-      if (event.target.nodeName.toLowerCase() === "a" ||
-          event.target.parentNode.nodeName.toLowerCase() === "a") {
-        return;
-      }
-
       this.jsterm.focus();
     });
 
     const serviceContainer = {
       attachRefToHud,
       emitNewMessage: (node, messageId) => {
         this.jsterm.hud.emit("new-messages", new Set([{
           node,
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
@@ -35,16 +35,17 @@ skip-if = (os == 'linux' && bits == 32 &
 [browser_webconsole_filters.js]
 [browser_webconsole_filters_persist.js]
 [browser_webconsole_init.js]
 [browser_webconsole_input_focus.js]
 [browser_webconsole_keyboard_accessibility.js]
 [browser_webconsole_location_debugger_link.js]
 [browser_webconsole_location_scratchpad_link.js]
 [browser_webconsole_location_styleeditor_link.js]
+[browser_webconsole_logErrorInPage.js]
 [browser_webconsole_network_messages_click.js]
 [browser_webconsole_nodes_highlight.js]
 [browser_webconsole_nodes_select.js]
 [browser_webconsole_object_inspector.js]
 [browser_webconsole_observer_notifications.js]
 [browser_webconsole_shows_reqs_in_netmonitor.js]
 [browser_webconsole_stacktrace_location_debugger_link.js]
 [browser_webconsole_stacktrace_location_scratchpad_link.js]
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_input_focus.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_input_focus.js
@@ -10,43 +10,51 @@
 const TEST_URI =
   `data:text/html;charset=utf-8,Test input focused
   <script>
     console.log("console message 1");
   </script>`;
 
 add_task(function* () {
   let hud = yield openNewTabAndConsole(TEST_URI);
+
   hud.jsterm.clearOutput();
-
   let inputNode = hud.jsterm.inputNode;
   ok(inputNode.getAttribute("focused"), "input node is focused after output is cleared");
 
+  info("Focus during message logging");
   ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
     content.wrappedJSObject.console.log("console message 2");
   });
   let msg = yield waitFor(() => findMessage(hud, "console message 2"));
-  let outputItem = msg.querySelector(".message-body");
-  inputNode = hud.jsterm.inputNode;
-  ok(inputNode.getAttribute("focused"), "input node is focused, first");
-  yield waitForBlurredInput(inputNode);
-  EventUtils.sendMouseEvent({type: "click"}, hud.outputNode);
+  ok(inputNode.getAttribute("focused"), "input node is focused, first time");
 
+  info("Focus after clicking in the output area");
+  yield waitForBlurredInput(hud);
+  EventUtils.sendMouseEvent({type: "click"}, msg);
   ok(inputNode.getAttribute("focused"), "input node is focused, second time");
-  yield waitForBlurredInput(inputNode);
+
   info("Setting a text selection and making sure a click does not re-focus");
+  yield waitForBlurredInput(hud);
   let selection = hud.iframeWindow.getSelection();
-  selection.selectAllChildren(outputItem);
-  EventUtils.sendMouseEvent({type: "click"}, hud.outputNode);
+  selection.selectAllChildren(msg.querySelector(".message-body"));
+  EventUtils.sendMouseEvent({type: "click"}, msg);
   ok(!inputNode.getAttribute("focused"),
-    "input node focused after text is selected");
+    "input node not focused after text is selected");
 });
 
-function waitForBlurredInput(inputNode) {
+function waitForBlurredInput(hud) {
+  let inputNode = hud.jsterm.inputNode;
   return new Promise(resolve => {
     let lostFocus = () => {
       ok(!inputNode.getAttribute("focused"), "input node is not focused");
       resolve();
     };
     inputNode.addEventListener("blur", lostFocus, { once: true });
+
+    // Clicking on a DOM Node outside of the webconsole document. The 'blur' event fires
+    // if we click on something in this document (like the filter box), but the 'focus'
+    // event won't re-fire on the textbox XBL binding when it's clicked on again.
+    // Bug 1304328 is tracking removal of XUL for jsterm, we should be able to click on
+    // the filter textbox instead of the url bar after that.
     document.getElementById("urlbar").click();
   });
 }
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_logErrorInPage.js
@@ -0,0 +1,20 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that we can log a message to the web console from the toolbox.
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>test logErrorInPage";
+
+add_task(async function () {
+  const hud = await openNewTabAndConsole(TEST_URI);
+  const toolbox = hud.ui.newConsoleOutput.toolbox;
+
+  toolbox.target.logErrorInPage("beware the octopus", "content javascript");
+
+  const node = await waitFor(() => findMessage(hud, "octopus"));
+  ok(node, "text is displayed in web console");
+});
--- a/devtools/server/actors/stylesheets.js
+++ b/devtools/server/actors/stylesheets.js
@@ -926,19 +926,24 @@ var StyleSheetsActor = protocol.ActorCla
   _getImported: function (doc, styleSheet) {
     return Task.spawn(function* () {
       let rules = yield styleSheet.getCSSRules();
       let imported = [];
 
       for (let i = 0; i < rules.length; i++) {
         let rule = rules[i];
         if (rule.type == Ci.nsIDOMCSSRule.IMPORT_RULE) {
-          // Associated styleSheet may be null if it has already been seen due
-          // to duplicate @imports for the same URL.
-          if (!rule.styleSheet || !this._shouldListSheet(doc, rule.styleSheet)) {
+          // With the Gecko style system, the associated styleSheet may be null
+          // if it has already been seen because an import cycle for the same
+          // URL.  With Stylo, the styleSheet will exist (which is correct per
+          // the latest CSSOM spec), so we also need to check ancestors for the
+          // same URL to avoid cycles.
+          let sheet = rule.styleSheet;
+          if (!sheet || this._haveAncestorWithSameURL(sheet) ||
+              !this._shouldListSheet(doc, sheet)) {
             continue;
           }
           let actor = this.parentActor.createStyleSheetActor(rule.styleSheet);
           imported.push(actor);
 
           // recurse imports in this stylesheet as well
           let children = yield this._getImported(doc, actor);
           imported = imported.concat(children);
@@ -948,16 +953,33 @@ var StyleSheetsActor = protocol.ActorCla
         }
       }
 
       return imported;
     }.bind(this));
   },
 
   /**
+   * Check all ancestors to see if this sheet's URL matches theirs as a way to
+   * detect an import cycle.
+   *
+   * @param {DOMStyleSheet} sheet
+   */
+  _haveAncestorWithSameURL(sheet) {
+    let sheetHref = sheet.href;
+    while (sheet.parentStyleSheet) {
+      if (sheet.parentStyleSheet.href == sheetHref) {
+        return true;
+      }
+      sheet = sheet.parentStyleSheet;
+    }
+    return false;
+  },
+
+  /**
    * Create a new style sheet in the document with the given text.
    * Return an actor for it.
    *
    * @param  {object} request
    *         Debugging protocol request object, with 'text property'
    * @return {object}
    *         Object with 'styelSheet' property for form on new actor.
    */
--- a/devtools/server/actors/tab.js
+++ b/devtools/server/actors/tab.js
@@ -7,17 +7,17 @@
 "use strict";
 
 /* global XPCNativeWrapper */
 
 // For performance matters, this file should only be loaded in the targeted
 // document process. For example, it shouldn't be evaluated in the parent
 // process until we try to debug a document living in the parent process.
 
-var { Ci, Cu, Cr } = require("chrome");
+var { Ci, Cu, Cr, Cc } = require("chrome");
 var Services = require("Services");
 var { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
 var promise = require("promise");
 var {
   ActorPool, createExtraActors, appendExtraActors
 } = require("devtools/server/actors/common");
 var { DebuggerServer } = require("devtools/server/main");
 var DevToolsUtils = require("devtools/shared/DevToolsUtils");
@@ -221,17 +221,19 @@ function TabActor(connection) {
 
   this.traits = {
     reconfigure: true,
     // Supports frame listing via `listFrames` request and `frameUpdate` events
     // as well as frame switching via `switchToFrame` request
     frames: true,
     // Do not require to send reconfigure request to reset the document state
     // to what it was before using the TabActor
-    noTabReconfigureOnClose: true
+    noTabReconfigureOnClose: true,
+    // Supports the logErrorInPage request.
+    logErrorInPage: true,
   };
 
   this._workerActorList = null;
   this._workerActorPool = null;
   this._onWorkerActorListChanged = this._onWorkerActorListChanged.bind(this);
 }
 
 // XXX (bug 710213): TabActor attach/detach/exit/destroy is a
@@ -656,16 +658,27 @@ TabActor.prototype = {
 
       return {
         "from": this.actorID,
         "workers": actors.map((actor) => actor.form())
       };
     });
   },
 
+  onLogErrorInPage(request) {
+    let {text, category} = request;
+    let scriptErrorClass = Cc["@mozilla.org/scripterror;1"];
+    let scriptError = scriptErrorClass.createInstance(Ci.nsIScriptError);
+    scriptError.initWithWindowID(text, null, null, 0, 0, 1,
+                                 category, getInnerId(this.window));
+    let console = Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService);
+    console.logMessage(scriptError);
+    return {};
+  },
+
   _onWorkerActorListChanged() {
     this._workerActorList.onListChanged = null;
     this.conn.sendActorEvent(this.actorID, "workerListChanged");
   },
 
   observe(subject, topic, data) {
     // Ignore any event that comes before/after the tab actor is attached
     // That typically happens during firefox shutdown.
@@ -1421,16 +1434,17 @@ TabActor.prototype.requestTypes = {
   "detach": TabActor.prototype.onDetach,
   "focus": TabActor.prototype.onFocus,
   "reload": TabActor.prototype.onReload,
   "navigateTo": TabActor.prototype.onNavigateTo,
   "reconfigure": TabActor.prototype.onReconfigure,
   "switchToFrame": TabActor.prototype.onSwitchToFrame,
   "listFrames": TabActor.prototype.onListFrames,
   "listWorkers": TabActor.prototype.onListWorkers,
+  "logErrorInPage": TabActor.prototype.onLogErrorInPage,
 };
 
 exports.TabActor = TabActor;
 
 /**
  * The DebuggerProgressListener object is an nsIWebProgressListener which
  * handles onStateChange events for the inspected browser. If the user tries to
  * navigate away from a paused page, the listener makes sure that the debuggee
--- a/devtools/server/primitive.js
+++ b/devtools/server/primitive.js
@@ -1,15 +1,13 @@
 /* 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 { Class } = require("sdk/core/heritage");
-
 const WebGLPrimitivesType = {
   "POINTS": 0,
   "LINES": 1,
   "LINE_LOOP": 2,
   "LINE_STRIP": 3,
   "TRIANGLES": 4,
   "TRIANGLE_STRIP": 5,
   "TRIANGLE_FAN": 6
@@ -17,69 +15,70 @@ const WebGLPrimitivesType = {
 
 /**
  * A utility for monitoring WebGL primitive draws. Takes a `tabActor`
  * and monitors primitive draws over time.
  */
 const WebGLDrawArrays = "drawArrays";
 const WebGLDrawElements = "drawElements";
 
-exports.WebGLPrimitiveCounter = Class({
-  initialize: function (tabActor) {
+exports.WebGLPrimitiveCounter = class WebGLPrimitiveCounter {
+
+  constructor(tabActor) {
     this.tabActor = tabActor;
-  },
+  }
 
-  destroy: function () {},
+  destroy() {}
 
   /**
    * Starts monitoring primitive draws, storing the primitives count per tick.
    */
-  resetCounts: function () {
+  resetCounts() {
     this._tris = 0;
     this._vertices = 0;
     this._points = 0;
     this._lines = 0;
     this._startTime = this.tabActor.docShell.now();
-  },
+  }
 
   /**
    * Stops monitoring primitive draws, returning the recorded values.
    */
-  getCounts: function () {
+  getCounts() {
     let result = {
       tris: this._tris,
       vertices: this._vertices,
       points: this._points,
       lines: this._lines
     };
 
     this._tris = 0;
     this._vertices = 0;
     this._points = 0;
     this._lines = 0;
     return result;
-  },
+  }
 
   /**
    * Handles WebGL draw primitive functions to catch primitive info.
    */
-  handleDrawPrimitive: function (functionCall) {
+  handleDrawPrimitive(functionCall) {
     let { name, args } = functionCall.details;
 
     if (name === WebGLDrawArrays) {
       this._processDrawArrays(args);
     } else if (name === WebGLDrawElements) {
       this._processDrawElements(args);
     }
-  },
+  }
 
   /**
    * Processes WebGL drawArrays method to count primitve numbers
    */
-  _processDrawArrays: function (args) {
+  _processDrawArrays(args) {
     let mode = args[0];
     let count = args[2];
 
     switch (mode) {
       case WebGLPrimitivesType.POINTS:
         this._vertices += count;
         this._points += count;
         break;
@@ -106,22 +105,22 @@ exports.WebGLPrimitiveCounter = Class({
       case WebGLPrimitivesType.TRIANGLE_FAN:
         this._tris += (count - 2);
         this._vertices += count;
         break;
       default:
         console.error("_processDrawArrays doesn't define this type.");
         break;
     }
-  },
+  }
 
   /**
    * Processes WebGL drawElements method to count primitve numbers
    */
-  _processDrawElements: function (args) {
+  _processDrawElements(args) {
     let mode = args[0];
     let count = args[1];
 
     switch (mode) {
       case WebGLPrimitivesType.POINTS:
         this._vertices += count;
         this._points += count;
         break;
@@ -155,9 +154,9 @@ exports.WebGLPrimitiveCounter = Class({
         this._tris += (count - 2);
         this._vertices += count;
         break;
       default:
         console.error("_processDrawElements doesn't define this type.");
         break;
     }
   }
-});
+};
--- a/devtools/shared/platform/chrome/clipboard.js
+++ b/devtools/shared/platform/chrome/clipboard.js
@@ -4,25 +4,58 @@
 
 // Helpers for clipboard handling.
 
 "use strict";
 
 const {Cc, Ci} = require("chrome");
 const clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"]
       .getService(Ci.nsIClipboardHelper);
-var clipboard = require("sdk/clipboard");
+const clipboardService = Cc["@mozilla.org/widget/clipboard;1"]
+      .getService(Ci.nsIClipboard);
 
 function copyString(string) {
   clipboardHelper.copyString(string);
 }
 
-function getCurrentFlavors() {
-  return clipboard.currentFlavors;
-}
+/**
+ * Retrieve the current clipboard data matching the flavor "text/unicode".
+ *
+ * @return {String} Clipboard text content, null if no text clipboard data is available.
+ */
+function getText() {
+  let flavor = "text/unicode";
+
+  let xferable = Cc["@mozilla.org/widget/transferable;1"]
+                 .createInstance(Ci.nsITransferable);
+
+  if (!xferable) {
+    throw new Error("Couldn't get the clipboard data due to an internal error " +
+                    "(couldn't create a Transferable object).");
+  }
+
+  xferable.init(null);
+  xferable.addDataFlavor(flavor);
 
-function getData() {
-  return clipboard.get();
+  // Get the data into our transferable.
+  clipboardService.getData(
+    xferable,
+    clipboardService.kGlobalClipboard
+  );
+
+  let data = {};
+  try {
+    xferable.getTransferData(flavor, data, {});
+  } catch (e) {
+    // Clipboard doesn't contain data in flavor, return null.
+    return null;
+  }
+
+  // There's no data available, return.
+  if (!data.value) {
+    return null;
+  }
+
+  return data.value.QueryInterface(Ci.nsISupportsString).data;
 }
 
 exports.copyString = copyString;
-exports.getCurrentFlavors = getCurrentFlavors;
-exports.getData = getData;
+exports.getText = getText;
--- a/devtools/shared/platform/content/clipboard.js
+++ b/devtools/shared/platform/content/clipboard.js
@@ -14,21 +14,15 @@ function copyString(string) {
     e.preventDefault();
   };
 
   document.addEventListener("copy", doCopy);
   document.execCommand("copy", false, null);
   document.removeEventListener("copy", doCopy);
 }
 
-function getCurrentFlavors() {
-  // See bug 1295692.
-  return [];
-}
-
-function getData() {
+function getText() {
   // See bug 1295692.
   return null;
 }
 
 exports.copyString = copyString;
-exports.getCurrentFlavors = getCurrentFlavors;
-exports.getData = getData;
+exports.getText = getText;
--- a/dom/animation/AnimValuesStyleRule.cpp
+++ b/dom/animation/AnimValuesStyleRule.cpp
@@ -1,26 +1,26 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "AnimValuesStyleRule.h"
+#include "mozilla/GeckoStyleContext.h"
 #include "nsRuleData.h"
-#include "mozilla/GeckoStyleContext.h"
 
 namespace mozilla {
 
 NS_IMPL_ISUPPORTS(AnimValuesStyleRule, nsIStyleRule)
 
 void
 AnimValuesStyleRule::MapRuleInfoInto(nsRuleData* aRuleData)
 {
-  GeckoStyleContext *contextParent = aRuleData->mStyleContext->GetParent();
+  GeckoStyleContext* contextParent = aRuleData->mStyleContext->GetParent();
   if (contextParent && contextParent->HasPseudoElementData()) {
     // Don't apply transitions or animations to things inside of
     // pseudo-elements.
     // FIXME (Bug 522599): Add tests for this.
 
     // Prevent structs from being cached on the rule node since we're inside
     // a pseudo-element, as we could determine cacheability differently
     // when walking the rule tree for a style context that is not inside
--- a/dom/interfaces/base/nsIDOMWindowUtils.idl
+++ b/dom/interfaces/base/nsIDOMWindowUtils.idl
@@ -254,17 +254,17 @@ interface nsIDOMWindowUtils : nsISupport
    * Whether the next paint should be flagged as the first paint for a document.
    * This gives a way to track the next paint that occurs after the flag is
    * set. The flag gets cleared after the next paint.
    *
    * Can only be accessed with chrome privileges.
    */
   attribute boolean isFirstPaint;
 
-  void getPresShellId(out uint32_t aPresShellId);
+  uint32_t getPresShellId();
 
   /**
    * Following modifiers are for sent*Event() except sendNative*Event().
    * NOTE: MODIFIER_ALT, MODIFIER_CONTROL, MODIFIER_SHIFT and MODIFIER_META
    *       are must be same values as nsIDOMNSEvent::*_MASK for backward
    *       compatibility.
    */
   const long MODIFIER_ALT        = 0x0001;
--- a/dom/interfaces/base/nsITabParent.idl
+++ b/dom/interfaces/base/nsITabParent.idl
@@ -2,16 +2,18 @@
  * 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 "domstubs.idl"
 
 interface nsIPrincipal;
 
+typedef unsigned long long nsViewID;
+
 [builtinclass, scriptable, uuid(8e49f7b0-1f98-4939-bf91-e9c39cd56434)]
 interface nsITabParent : nsISupports
 {
   void getChildProcessOffset(out int32_t aCssX, out int32_t aCssY);
 
   readonly attribute boolean useAsyncPanZoom;
 
   /**
@@ -70,9 +72,25 @@ interface nsITabParent : nsISupports
    */
   void transmitPermissionsForPrincipal(in nsIPrincipal aPrincipal);
 
   /**
    * True if any of the frames loaded in the TabChild have registered
    * an onbeforeunload event handler.
    */
   readonly attribute boolean hasBeforeUnload;
+
+  /**
+   * Notify APZ to start autoscrolling.
+   * (aAnchorX, aAnchorY) are the coordinates of the autoscroll anchor,
+   * in LayoutDevice coordinates relative to the screen. aScrollId and 
+   * aPresShellId identify the scroll frame that content chose to scroll.
+   */
+  void startApzAutoscroll(in float aAnchorX, in float aAnchorY,
+                          in nsViewID aScrollId, in uint32_t aPresShellId);
+
+  /**
+   * Notify APZ to stop autoscrolling.
+   * aScrollId and aPresShellId identify the scroll frame that is being
+   * autoscrolled.
+   */
+  void stopApzAutoscroll(in nsViewID aScrollId, in uint32_t aPresShellId);
 };
--- a/dom/ipc/TabParent.cpp
+++ b/dom/ipc/TabParent.cpp
@@ -3258,16 +3258,60 @@ TabParent::StartPersistence(uint64_t aOu
   auto* actor = new WebBrowserPersistDocumentParent();
   actor->SetOnReady(aRecv);
   return manager->AsContentParent()
     ->SendPWebBrowserPersistDocumentConstructor(actor, this, aOuterWindowID)
     ? NS_OK : NS_ERROR_FAILURE;
   // (The actor will be destroyed on constructor failure.)
 }
 
+NS_IMETHODIMP
+TabParent::StartApzAutoscroll(float aAnchorX, float aAnchorY,
+                              nsViewID aScrollId, uint32_t aPresShellId)
+{
+  if (!AsyncPanZoomEnabled()) {
+    return NS_OK;
+  }
+
+  if (RenderFrameParent* renderFrame = GetRenderFrame()) {
+    uint64_t layersId = renderFrame->GetLayersId();
+    if (nsCOMPtr<nsIWidget> widget = GetWidget()) {
+      ScrollableLayerGuid guid{layersId, aPresShellId, aScrollId};
+
+      // The anchor coordinates that are passed in are relative to the origin
+      // of the screen, but we are sending them to APZ which only knows about
+      // coordinates relative to the widget, so convert them accordingly.
+      LayoutDeviceIntPoint anchor = RoundedToInt(LayoutDevicePoint{aAnchorX, aAnchorY});
+      anchor -= widget->WidgetToScreenOffset();
+
+      widget->StartAsyncAutoscroll(
+          ViewAs<ScreenPixel>(anchor, PixelCastJustification::LayoutDeviceIsScreenForBounds),
+          guid);
+    }
+  }
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+TabParent::StopApzAutoscroll(nsViewID aScrollId, uint32_t aPresShellId)
+{
+  if (!AsyncPanZoomEnabled()) {
+    return NS_OK;
+  }
+
+  if (RenderFrameParent* renderFrame = GetRenderFrame()) {
+    uint64_t layersId = renderFrame->GetLayersId();
+    if (nsCOMPtr<nsIWidget> widget = GetWidget()) {
+      ScrollableLayerGuid guid{layersId, aPresShellId, aScrollId};
+      widget->StopAsyncAutoscroll(guid);
+    }
+  }
+  return NS_OK;
+}
+
 ShowInfo
 TabParent::GetShowInfo()
 {
   TryCacheDPIAndScale();
   if (mFrameElement) {
     nsAutoString name;
     mFrameElement->GetAttr(kNameSpaceID_None, nsGkAtoms::name, name);
     bool allowFullscreen =
--- a/gfx/layers/FrameMetrics.h
+++ b/gfx/layers/FrameMetrics.h
@@ -330,16 +330,22 @@ public:
     return mIsRootContent;
   }
 
   void SetScrollOffset(const CSSPoint& aScrollOffset)
   {
     mScrollOffset = aScrollOffset;
   }
 
+  // Set scroll offset, first clamping to the scroll range.
+  void ClampAndSetScrollOffset(const CSSPoint& aScrollOffset)
+  {
+    SetScrollOffset(CalculateScrollRange().ClampPoint(aScrollOffset));
+  }
+
   const CSSPoint& GetScrollOffset() const
   {
     return mScrollOffset;
   }
 
   void SetSmoothScrollOffset(const CSSPoint& aSmoothScrollDestination)
   {
     mSmoothScrollOffset = aSmoothScrollDestination;
--- a/gfx/layers/apz/public/GeckoContentController.h
+++ b/gfx/layers/apz/public/GeckoContentController.h
@@ -152,16 +152,18 @@ public:
 
   /**
    * Notify content that the repaint requests have been flushed.
    */
   virtual void NotifyFlushComplete() = 0;
 
   virtual void NotifyAsyncScrollbarDragRejected(const FrameMetrics::ViewID& aScrollId) = 0;
 
+  virtual void NotifyAutoscrollHandledByAPZ(const FrameMetrics::ViewID& aScrollId) = 0;
+
   virtual void UpdateOverscrollVelocity(float aX, float aY, bool aIsRootContent) {}
   virtual void UpdateOverscrollOffset(float aX, float aY, bool aIsRootContent) {}
 
   GeckoContentController() {}
 
   /**
    * Needs to be called on the main thread.
    */
--- a/gfx/layers/apz/public/IAPZCTreeManager.h
+++ b/gfx/layers/apz/public/IAPZCTreeManager.h
@@ -178,16 +178,22 @@ public:
   virtual void SetAllowedTouchBehavior(
       uint64_t aInputBlockId,
       const nsTArray<TouchBehaviorFlags>& aValues) = 0;
 
   virtual void StartScrollbarDrag(
       const ScrollableLayerGuid& aGuid,
       const AsyncDragMetrics& aDragMetrics) = 0;
 
+  virtual void StartAutoscroll(
+      const ScrollableLayerGuid& aGuid,
+      const ScreenPoint& aAnchorLocation) = 0;
+
+  virtual void StopAutoscroll(const ScrollableLayerGuid& aGuid) = 0;
+
   /**
    * Function used to disable LongTap gestures.
    *
    * On slow running tests, drags and touch events can be misinterpreted
    * as a long tap. This allows tests to disable long tap gesture detection.
    */
   virtual void SetLongTapEnabled(bool aTapGestureEnabled) = 0;
 
--- a/gfx/layers/apz/src/APZCTreeManager.cpp
+++ b/gfx/layers/apz/src/APZCTreeManager.cpp
@@ -665,16 +665,33 @@ APZCTreeManager::StartScrollbarDrag(cons
     return;
   }
 
   uint64_t inputBlockId = aDragMetrics.mDragStartSequenceNumber;
   mInputQueue->ConfirmDragBlock(inputBlockId, apzc, aDragMetrics);
 }
 
 void
+APZCTreeManager::StartAutoscroll(const ScrollableLayerGuid& aGuid,
+                                 const ScreenPoint& aAnchorLocation)
+{
+  if (RefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(aGuid)) {
+    apzc->StartAutoscroll(aAnchorLocation);
+  }
+}
+
+void
+APZCTreeManager::StopAutoscroll(const ScrollableLayerGuid& aGuid)
+{
+  if (RefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(aGuid)) {
+    apzc->StopAutoscroll();
+  }
+}
+
+void
 APZCTreeManager::NotifyScrollbarDragRejected(const ScrollableLayerGuid& aGuid) const
 {
   const LayerTreeState* state = CompositorBridgeParent::GetIndirectShadowTree(aGuid.mLayersId);
   MOZ_ASSERT(state && state->mController);
   state->mController->NotifyAsyncScrollbarDragRejected(aGuid.mScrollId);
 }
 
 template<class ScrollNode> HitTestingTreeNode*
@@ -981,16 +998,18 @@ APZCTreeManager::ReceiveInputEvent(Input
     case MULTITOUCH_INPUT: {
       MultiTouchInput& touchInput = aEvent.AsMultiTouchInput();
       result = ProcessTouchInput(touchInput, aOutTargetGuid, aOutInputBlockId);
       break;
     } case MOUSE_INPUT: {
       MouseInput& mouseInput = aEvent.AsMouseInput();
       mouseInput.mHandledByAPZ = true;
 
+      mCurrentMousePosition = mouseInput.mOrigin;
+
       bool startsDrag = DragTracker::StartsDrag(mouseInput);
       if (startsDrag) {
         // If this is the start of a drag we need to unambiguously know if it's
         // going to land on a scrollbar or not. We can't apply an untransform
         // here without knowing that, so we need to ensure the untransform is
         // a no-op.
         FlushRepaintsToClearScreenToGeckoTransform();
       }
@@ -2473,16 +2492,22 @@ APZCTreeManager::GetApzcToGeckoTransform
     // The above value for result when parent == P matches the required output
     // as explained in the comment above this method. Note that any missing
     // terms are guaranteed to be identity transforms.
   }
 
   return ViewAs<ParentLayerToScreenMatrix4x4>(result);
 }
 
+ScreenPoint
+APZCTreeManager::GetCurrentMousePosition() const
+{
+  return mCurrentMousePosition;
+}
+
 already_AddRefed<AsyncPanZoomController>
 APZCTreeManager::GetMultitouchTarget(AsyncPanZoomController* aApzc1, AsyncPanZoomController* aApzc2) const
 {
   MutexAutoLock lock(mTreeLock);
   RefPtr<AsyncPanZoomController> apzc;
   // For now, we only ever want to do pinching on the root-content APZC for
   // a given layers id.
   if (aApzc1 && aApzc2 && aApzc1->GetLayersId() == aApzc2->GetLayersId()) {
--- a/gfx/layers/apz/src/APZCTreeManager.h
+++ b/gfx/layers/apz/src/APZCTreeManager.h
@@ -430,16 +430,21 @@ public:
    */
   void DispatchFling(AsyncPanZoomController* aApzc,
                      FlingHandoffState& aHandoffState);
 
   void StartScrollbarDrag(
       const ScrollableLayerGuid& aGuid,
       const AsyncDragMetrics& aDragMetrics) override;
 
+  void StartAutoscroll(const ScrollableLayerGuid& aGuid,
+                       const ScreenPoint& aAnchorLocation) override;
+
+  void StopAutoscroll(const ScrollableLayerGuid& aGuid) override;
+
   /*
    * Build the chain of APZCs that will handle overscroll for a pan starting at |aInitialTarget|.
    */
   RefPtr<const OverscrollHandoffChain> BuildOverscrollHandoffChain(const RefPtr<AsyncPanZoomController>& aInitialTarget);
 
   /**
    * Function used to disable LongTap gestures.
    *
@@ -480,16 +485,17 @@ public:
   RefPtr<HitTestingTreeNode> GetRootNode() const;
   already_AddRefed<AsyncPanZoomController> GetTargetAPZC(const ScreenPoint& aPoint,
                                                          HitTestResult* aOutHitResult,
                                                          RefPtr<HitTestingTreeNode>* aOutScrollbarNode = nullptr);
   already_AddRefed<AsyncPanZoomController> GetTargetAPZC(const uint64_t& aLayersId,
                                                          const FrameMetrics::ViewID& aScrollId);
   ScreenToParentLayerMatrix4x4 GetScreenToApzcTransform(const AsyncPanZoomController *aApzc) const;
   ParentLayerToScreenMatrix4x4 GetApzcToGeckoTransform(const AsyncPanZoomController *aApzc) const;
+  ScreenPoint GetCurrentMousePosition() const;
 
   /**
    * Process touch velocity.
    * Sometimes the touch move event will have a velocity even though no scrolling
    * is occurring such as when the toolbar is being hidden/shown in Fennec.
    * This function can be called to have the y axis' velocity queue updated.
    */
   void ProcessTouchVelocity(uint32_t aTimestampMs, float aSpeedY) override;
@@ -592,16 +598,19 @@ private:
   /* Sometimes we want to ignore all touches except one. In such cases, this
    * is set to the identifier of the touch we are not ignoring; in other cases,
    * this is set to -1.
    */
   int32_t mRetainedTouchIdentifier;
   /* Tracks the number of touch points we are tracking that are currently on
    * the screen. */
   TouchCounter mTouchCounter;
+  /* Stores the current mouse position in screen coordinates.
+   */
+  ScreenPoint mCurrentMousePosition;
   /* For logging the APZC tree for debugging (enabled by the apz.printtree
    * pref). */
   gfx::TreeLog mApzcTreeLog;
 
   class CheckerboardFlushObserver;
   friend class CheckerboardFlushObserver;
   RefPtr<CheckerboardFlushObserver> mFlushObserver;
 
--- a/gfx/layers/apz/src/AsyncPanZoomController.cpp
+++ b/gfx/layers/apz/src/AsyncPanZoomController.cpp
@@ -4,16 +4,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include <math.h>                       // for fabsf, fabs, atan2
 #include <stdint.h>                     // for uint32_t, uint64_t
 #include <sys/types.h>                  // for int32_t
 #include <algorithm>                    // for max, min
 #include "AsyncPanZoomController.h"     // for AsyncPanZoomController, etc
+#include "AutoscrollAnimation.h"        // for AutoscrollAnimation
 #include "Axis.h"                       // for AxisX, AxisY, Axis, etc
 #include "CheckerboardEvent.h"          // for CheckerboardEvent
 #include "Compositor.h"                 // for Compositor
 #include "FrameMetrics.h"               // for FrameMetrics, etc
 #include "GenericFlingAnimation.h"      // for GenericFlingAnimation
 #include "GestureEventListener.h"       // for GestureEventListener
 #include "HitTestingTreeNode.h"         // for HitTestingTreeNode
 #include "InputData.h"                  // for MultiTouchInput, etc
@@ -129,16 +130,20 @@ typedef GenericFlingAnimation FlingAnima
  * \li\b apz.allow_checkerboarding
  * Pref that allows or disallows checkerboarding
  *
  * \li\b apz.allow_immediate_handoff
  * If set to true, scroll can be handed off from one APZC to another within
  * a single input block. If set to false, a single input block can only
  * scroll one APZC.
  *
+ * \li\b apz.autoscroll.enabled
+ * If set to true, autoscrolling is driven by APZ rather than the content
+ * process main thread.
+ *
  * \li\b apz.axis_lock.mode
  * The preferred axis locking style. See AxisLockMode for possible values.
  *
  * \li\b apz.axis_lock.lock_angle
  * Angle from axis within which we stay axis-locked.\n
  * Units: radians
  *
  * \li\b apz.axis_lock.breakout_threshold
@@ -577,20 +582,19 @@ public:
     nsPoint oneParentLayerPixel =
       CSSPoint::ToAppUnits(ParentLayerPoint(1, 1) / aFrameMetrics.GetZoom());
     if (mXAxisModel.IsFinished(oneParentLayerPixel.x) &&
         mYAxisModel.IsFinished(oneParentLayerPixel.y)) {
       // Set the scroll offset to the exact destination. If we allow the scroll
       // offset to end up being a bit off from the destination, we can get
       // artefacts like "scroll to the next snap point in this direction"
       // scrolling to the snap point we're already supposed to be at.
-      aFrameMetrics.SetScrollOffset(
-          aFrameMetrics.CalculateScrollRange().ClampPoint(
-              CSSPoint::FromAppUnits(nsPoint(mXAxisModel.GetDestination(),
-                                             mYAxisModel.GetDestination()))));
+      aFrameMetrics.ClampAndSetScrollOffset(
+          CSSPoint::FromAppUnits(nsPoint(mXAxisModel.GetDestination(),
+                                         mYAxisModel.GetDestination())));
       return false;
     }
 
     mXAxisModel.Simulate(aDelta);
     mYAxisModel.Simulate(aDelta);
 
     CSSPoint position = CSSPoint::FromAppUnits(nsPoint(mXAxisModel.GetPosition(),
                                                        mYAxisModel.GetPosition()));
@@ -1066,29 +1070,51 @@ nsEventStatus AsyncPanZoomController::Ha
   return rv;
 }
 
 void AsyncPanZoomController::HandleTouchVelocity(uint32_t aTimesampMs, float aSpeedY)
 {
   mY.HandleTouchVelocity(aTimesampMs, aSpeedY);
 }
 
+void AsyncPanZoomController::StartAutoscroll(const ScreenPoint& aPoint)
+{
+  // Cancel any existing animation.
+  CancelAnimation();
+
+  SetState(AUTOSCROLL);
+  StartAnimation(new AutoscrollAnimation(*this, aPoint));
+
+  // Notify content that we are handlng the autoscroll.
+  if (RefPtr<GeckoContentController> controller = GetGeckoContentController()) {
+    controller->NotifyAutoscrollHandledByAPZ(mFrameMetrics.GetScrollId());
+  }
+}
+
+void AsyncPanZoomController::StopAutoscroll()
+{
+  if (mState == AUTOSCROLL) {
+    CancelAnimation();
+  }
+}
+
 nsEventStatus AsyncPanZoomController::OnTouchStart(const MultiTouchInput& aEvent) {
   APZC_LOG("%p got a touch-start in state %d\n", this, mState);
   mPanDirRestricted = false;
   ParentLayerPoint point = GetFirstTouchPoint(aEvent);
 
   switch (mState) {
     case FLING:
     case ANIMATING_ZOOM:
     case SMOOTH_SCROLL:
     case OVERSCROLL_ANIMATION:
     case WHEEL_SCROLL:
     case KEYBOARD_SCROLL:
     case PAN_MOMENTUM:
+    case AUTOSCROLL:
       MOZ_ASSERT(GetCurrentTouchBlock());
       GetCurrentTouchBlock()->GetOverscrollHandoffChain()->CancelAnimations(ExcludeOverscroll);
       MOZ_FALLTHROUGH;
     case NOTHING: {
       mX.StartTouch(point.x, aEvent.mTime);
       mY.StartTouch(point.y, aEvent.mTime);
       if (RefPtr<GeckoContentController> controller = GetGeckoContentController()) {
         MOZ_ASSERT(GetCurrentTouchBlock());
@@ -1155,16 +1181,17 @@ nsEventStatus AsyncPanZoomController::On
     case PINCHING:
       // The scale gesture listener should have handled this.
       NS_WARNING("Gesture listener should have handled pinching in OnTouchMove.");
       return nsEventStatus_eIgnore;
 
     case WHEEL_SCROLL:
     case KEYBOARD_SCROLL:
     case OVERSCROLL_ANIMATION:
+    case AUTOSCROLL:
       // Should not receive a touch-move in the OVERSCROLL_ANIMATION state
       // as touch blocks that begin in an overscrolled state cancel the
       // animation. The same is true for wheel scroll animations.
       NS_WARNING("Received impossible touch in OnTouchMove");
       break;
   }
 
   return nsEventStatus_eConsumeNoDefault;
@@ -1236,16 +1263,17 @@ nsEventStatus AsyncPanZoomController::On
     SetState(NOTHING);
     // Scale gesture listener should have handled this.
     NS_WARNING("Gesture listener should have handled pinching in OnTouchEnd.");
     return nsEventStatus_eIgnore;
 
   case WHEEL_SCROLL:
   case KEYBOARD_SCROLL:
   case OVERSCROLL_ANIMATION:
+  case AUTOSCROLL:
     // Should not receive a touch-end in the OVERSCROLL_ANIMATION state
     // as touch blocks that begin in an overscrolled state cancel the
     // animation. The same is true for WHEEL_SCROLL.
     NS_WARNING("Received impossible touch in OnTouchEnd");
     break;
   }
 
   return nsEventStatus_eConsumeNoDefault;
@@ -2912,16 +2940,20 @@ void AsyncPanZoomController::AdjustScrol
   RequestContentRepaint();
   UpdateSharedCompositorFrameMetrics();
 }
 
 void AsyncPanZoomController::ScrollBy(const CSSPoint& aOffset) {
   mFrameMetrics.ScrollBy(aOffset);
 }
 
+void AsyncPanZoomController::ScrollByAndClamp(const CSSPoint& aOffset) {
+  mFrameMetrics.ClampAndSetScrollOffset(mFrameMetrics.GetScrollOffset() + aOffset);
+}
+
 void AsyncPanZoomController::ScaleWithFocus(float aScale,
                                             const CSSPoint& aFocus) {
   mFrameMetrics.ZoomBy(aScale);
   // We want to adjust the scroll offset such that the CSS point represented by aFocus remains
   // at the same position on the screen before and after the change in zoom. The below code
   // accomplishes this; see https://bugzilla.mozilla.org/show_bug.cgi?id=923431#c6 for an
   // in-depth explanation of how.
   mFrameMetrics.SetScrollOffset((mFrameMetrics.GetScrollOffset() + aFocus) - (aFocus / aScale));
@@ -3784,19 +3816,17 @@ void AsyncPanZoomController::NotifyLayer
       // result in a displayport that doesn't extend upwards at all.
       // Note that even if the CancelAnimation call above requested a repaint
       // this is fine because we already have repaint request deduplication.
       needContentRepaint = true;
     } else if (scrollableRectChanged) {
       // Even if we didn't accept a new scroll offset from content, the
       // scrollable rect may have changed in a way that makes our local
       // scroll offset out of bounds, so re-clamp it.
-      mFrameMetrics.SetScrollOffset(
-          mFrameMetrics.CalculateScrollRange().ClampPoint(
-              mFrameMetrics.GetScrollOffset()));
+      mFrameMetrics.ClampAndSetScrollOffset(mFrameMetrics.GetScrollOffset());
     }
   }
 
   if (smoothScrollRequested) {
     // A smooth scroll has been requested for animation on the compositor
     // thread.  This flag will be reset by the main thread when it receives
     // the scroll update acknowledgement.
 
--- a/gfx/layers/apz/src/AsyncPanZoomController.h
+++ b/gfx/layers/apz/src/AsyncPanZoomController.h
@@ -284,16 +284,26 @@ public:
    * Handler for touch velocity.
    * Sometimes the touch move event will have a velocity even though no scrolling
    * is occurring such as when the toolbar is being hidden/shown in Fennec.
    * This function can be called to have the y axis' velocity queue updated.
    */
   void HandleTouchVelocity(uint32_t aTimesampMs, float aSpeedY);
 
   /**
+   * Start autoscrolling this APZC, anchored at the provided location.
+   */
+  void StartAutoscroll(const ScreenPoint& aAnchorLocation);
+
+  /**
+   * Stop autoscrolling this APZC.
+   */
+  void StopAutoscroll();
+
+  /**
    * Populates the provided object (if non-null) with the scrollable guid of this apzc.
    */
   void GetGuid(ScrollableLayerGuid* aGuidOut) const;
 
   /**
    * Returns the scrollable guid of this apzc.
    */
   ScrollableLayerGuid GetGuid() const;
@@ -527,21 +537,29 @@ protected:
    * Helper method to cancel any gesture currently going to Gecko. Used
    * primarily when a user taps the screen over some clickable content but then
    * pans down instead of letting go (i.e. to cancel a previous touch so that a
    * new one can properly take effect.
    */
   nsEventStatus OnCancelTap(const TapGestureInput& aEvent);
 
   /**
-   * Scrolls the viewport by an X,Y offset.
+   * Scroll the scroll frame by an X,Y offset.
+   * The resulting scroll offset is not clamped to the scrollable rect;
+   * the caller must ensure it stays within range.
    */
   void ScrollBy(const CSSPoint& aOffset);
 
   /**
+   * Scroll the scroll frame by an X,Y offset, clamping the resulting
+   * scroll offset to the scrollable rect.
+   */
+  void ScrollByAndClamp(const CSSPoint& aOffset);
+
+  /**
    * Scales the viewport by an amount (note that it multiplies this scale in to
    * the current scale, it doesn't set it to |aScale|). Also considers a focus
    * point so that the page zooms inward/outward from that point.
    */
   void ScaleWithFocus(float aScale,
                       const CSSPoint& aFocus);
 
   /**
@@ -886,17 +904,18 @@ protected:
 
     PINCHING,                 /* nth touch-start, where n > 1. this mode allows pan and zoom */
     ANIMATING_ZOOM,           /* animated zoom to a new rect */
     OVERSCROLL_ANIMATION,     /* Spring-based animation used to relieve overscroll once
                                  the finger is lifted. */
     SMOOTH_SCROLL,            /* Smooth scrolling to destination. Used by
                                  CSSOM-View smooth scroll-behavior */
     WHEEL_SCROLL,             /* Smooth scrolling to a destination for a wheel event. */
-    KEYBOARD_SCROLL           /* Smooth scrolling to a destination for a keyboard event. */
+    KEYBOARD_SCROLL,          /* Smooth scrolling to a destination for a keyboard event. */
+    AUTOSCROLL                /* Autoscroll animation. */
   };
 
   // This is in theory protected by |mMonitor|; that is, it should be held whenever
   // this is updated. In practice though... see bug 897017.
   PanZoomState mState;
 
 private:
   friend class StateChangeNotificationBlocker;
@@ -980,16 +999,17 @@ public:
    * |aHandoffState.mIsHandoff| should be true iff. the fling was handed off
    * from a previous APZC, and determines whether acceleration is applied
    * to the fling.
    */
   bool AttemptFling(FlingHandoffState& aHandoffState);
 
 private:
   friend class AndroidFlingAnimation;
+  friend class AutoscrollAnimation;
   friend class GenericFlingAnimation;
   friend class OverscrollAnimation;
   friend class SmoothScrollAnimation;
   friend class GenericScrollAnimation;
   friend class WheelScrollAnimation;
   friend class KeyboardScrollAnimation;
 
   friend class GenericOverscrollEffect;
new file mode 100644
--- /dev/null
+++ b/gfx/layers/apz/src/AutoscrollAnimation.cpp
@@ -0,0 +1,89 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AutoscrollAnimation.h"
+
+#include <cmath>  // for sqrtf()
+
+#include "AsyncPanZoomController.h"
+#include "mozilla/Telemetry.h"                  // for Telemetry
+#include "mozilla/layers/ScrollInputMethods.h"  // for ScrollInputMethod
+
+namespace mozilla {
+namespace layers {
+
+// Helper function for AutoscrollAnimation::DoSample().
+// Basically copied as-is from toolkit/content/browser-content.js.
+static float
+Accelerate(ScreenCoord curr, ScreenCoord start)
+{
+  static const int speed = 12;
+  float val = (curr - start) / speed;
+  if (val > 1) {
+    return val * sqrtf(val) - 1;
+  }
+  if (val < -1) {
+    return val * sqrtf(-val) + 1;
+  }
+  return 0;
+}
+
+AutoscrollAnimation::AutoscrollAnimation(AsyncPanZoomController& aApzc,
+                                         const ScreenPoint& aAnchorLocation)
+  : mApzc(aApzc)
+  , mAnchorLocation(aAnchorLocation)
+{
+}
+
+bool
+AutoscrollAnimation::DoSample(FrameMetrics& aFrameMetrics, const TimeDuration& aDelta)
+{
+  APZCTreeManager* treeManager = mApzc.GetApzcTreeManager();
+  if (!treeManager) {
+    return false;
+  }
+
+  Telemetry::Accumulate(Telemetry::SCROLL_INPUT_METHODS,
+      (uint32_t) ScrollInputMethod::ApzAutoscrolling);
+
+  ScreenPoint mouseLocation = treeManager->GetCurrentMousePosition();
+
+  // The implementation of this function closely mirrors that of its main-
+  // thread equivalent, the autoscrollLoop() function in
+  // toolkit/content/browser-content.js.
+
+  // Avoid long jumps when the browser hangs for more than |maxTimeDelta| ms.
+  static const TimeDuration maxTimeDelta = TimeDuration::FromMilliseconds(100);
+  TimeDuration timeDelta = TimeDuration::Max(aDelta, maxTimeDelta);
+
+  float timeCompensation = timeDelta.ToMilliseconds() / 20;
+
+  // Notes:
+  //   - The main-thread implementation rounds the scroll delta to an integer,
+  //     and keeps track of the fractional part as an "error". It does this
+  //     because it uses Window.scrollBy() or Element.scrollBy() to perform
+  //     the scrolling, and those functions truncate the fractional part of
+  //     the offset. APZ does no such truncation, so there's no need to keep
+  //     track of the fractional part separately.
+  //   - The Accelerate() function takes Screen coordinates as inputs, but
+  //     its output is interpreted as CSS coordinates. This is intentional,
+  //     insofar as autoscrollLoop() does the same thing.
+  CSSPoint scrollDelta{
+    Accelerate(mouseLocation.x, mAnchorLocation.x) * timeCompensation,
+    Accelerate(mouseLocation.y, mAnchorLocation.y) * timeCompensation
+  };
+
+  mApzc.ScrollByAndClamp(scrollDelta);
+
+  // An autoscroll animation never ends of its own accord.
+  // It can be stopped in response to various input events, in which case
+  // AsyncPanZoomController::StopAutoscroll() will stop it via
+  // CancelAnimation().
+  return true;
+}
+
+} // namespace layers
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/gfx/layers/apz/src/AutoscrollAnimation.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layers_AutocrollAnimation_h_
+#define mozilla_layers_AutocrollAnimation_h_
+
+#include "AsyncPanZoomAnimation.h"
+
+namespace mozilla {
+namespace layers {
+
+class AsyncPanZoomController;
+
+class AutoscrollAnimation : public AsyncPanZoomAnimation
+{
+public:
+  AutoscrollAnimation(AsyncPanZoomController& aApzc,
+                      const ScreenPoint& aAnchorLocation);
+
+  bool DoSample(FrameMetrics& aFrameMetrics, const TimeDuration& aDelta) override;
+
+private:
+  AsyncPanZoomController& mApzc;
+  ScreenPoint mAnchorLocation;
+};
+
+} // namespace layers
+} // namespace mozilla
+
+#endif // mozilla_layers_AutoscrollAnimation_h_
--- a/gfx/layers/apz/test/gtest/APZTestCommon.h
+++ b/gfx/layers/apz/test/gtest/APZTestCommon.h
@@ -88,16 +88,17 @@ public:
     return NS_IsMainThread();
   }
   void DispatchToRepaintThread(already_AddRefed<Runnable> aTask) {
     NS_DispatchToMainThread(Move(aTask));
   }
   MOCK_METHOD3(NotifyAPZStateChange, void(const ScrollableLayerGuid& aGuid, APZStateChange aChange, int aArg));
   MOCK_METHOD0(NotifyFlushComplete, void());
   MOCK_METHOD1(NotifyAsyncScrollbarDragRejected, void(const FrameMetrics::ViewID&));
+  MOCK_METHOD1(NotifyAutoscrollHandledByAPZ, void(const FrameMetrics::ViewID&));
 };
 
 class MockContentControllerDelayed : public MockContentController {
 public:
   MockContentControllerDelayed()
     : mTime(GetStartupTime())
   {
   }
--- a/gfx/layers/apz/util/APZCCallbackHelper.cpp
+++ b/gfx/layers/apz/util/APZCCallbackHelper.cpp
@@ -954,16 +954,28 @@ APZCCallbackHelper::NotifyAsyncScrollbar
 {
   MOZ_ASSERT(NS_IsMainThread());
   if (nsIScrollableFrame* scrollFrame = nsLayoutUtils::FindScrollableFrameFor(aScrollId)) {
     scrollFrame->AsyncScrollbarDragRejected();
   }
 }
 
 /* static */ void
+APZCCallbackHelper::NotifyAutoscrollHandledByAPZ(const FrameMetrics::ViewID& aScrollId)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  nsCOMPtr<nsIObserverService> observerService = mozilla::services::GetObserverService();
+  MOZ_ASSERT(observerService);
+
+  nsAutoString data;
+  data.AppendInt(aScrollId);
+  observerService->NotifyObservers(nullptr, "autoscroll-handled-by-apz", data.get());
+}
+
+/* static */ void
 APZCCallbackHelper::NotifyPinchGesture(PinchGestureInput::PinchGestureType aType,
                                        LayoutDeviceCoord aSpanChange,
                                        Modifiers aModifiers,
                                        nsIWidget* aWidget)
 {
   EventMessage msg;
   switch (aType) {
     case PinchGestureInput::PINCHGESTURE_START:
--- a/gfx/layers/apz/util/APZCCallbackHelper.h
+++ b/gfx/layers/apz/util/APZCCallbackHelper.h
@@ -161,16 +161,18 @@ public:
     /* Notify content of a mouse scroll testing event. */
     static void NotifyMozMouseScrollEvent(const FrameMetrics::ViewID& aScrollId, const nsString& aEvent);
 
     /* Notify content that the repaint flush is complete. */
     static void NotifyFlushComplete(nsIPresShell* aShell);
 
     static void NotifyAsyncScrollbarDragRejected(const FrameMetrics::ViewID& aScrollId);
 
+    static void NotifyAutoscrollHandledByAPZ(const FrameMetrics::ViewID& aScrollId);
+
     /* Temporarily ignore the Displayport for better paint performance. If at
      * all possible, pass in a presShell if you have one at the call site, we
      * use it to trigger a repaint once suppression is disabled. Without that
      * the displayport may get left at the suppressed size for an extended
      * period of time and result in unnecessary checkerboarding (see bug
      * 1255054). */
     static void SuppressDisplayport(const bool& aEnabled,
                                     const nsCOMPtr<nsIPresShell>& aShell);
--- a/gfx/layers/apz/util/ChromeProcessController.cpp
+++ b/gfx/layers/apz/util/ChromeProcessController.cpp
@@ -303,8 +303,23 @@ ChromeProcessController::NotifyAsyncScro
       this,
       &ChromeProcessController::NotifyAsyncScrollbarDragRejected,
       aScrollId));
     return;
   }
 
   APZCCallbackHelper::NotifyAsyncScrollbarDragRejected(aScrollId);
 }
+
+void
+ChromeProcessController::NotifyAutoscrollHandledByAPZ(const FrameMetrics::ViewID& aScrollId)
+{
+  if (MessageLoop::current() != mUILoop) {
+    mUILoop->PostTask(NewRunnableMethod<FrameMetrics::ViewID>(
+      "layers::ChromeProcessController::NotifyAutoscrollHandledByAPZ",
+      this,
+      &ChromeProcessController::NotifyAutoscrollHandledByAPZ,
+      aScrollId));
+    return;
+  }
+
+  APZCCallbackHelper::NotifyAutoscrollHandledByAPZ(aScrollId);
+}
--- a/gfx/layers/apz/util/ChromeProcessController.h
+++ b/gfx/layers/apz/util/ChromeProcessController.h
@@ -59,16 +59,17 @@ public:
                                   Modifiers aModifiers) override;
   virtual void NotifyAPZStateChange(const ScrollableLayerGuid& aGuid,
                                     APZStateChange aChange,
                                     int aArg) override;
   virtual void NotifyMozMouseScrollEvent(const FrameMetrics::ViewID& aScrollId,
                                          const nsString& aEvent) override;
   virtual void NotifyFlushComplete() override;
   virtual void NotifyAsyncScrollbarDragRejected(const FrameMetrics::ViewID& aScrollId) override;
+  virtual void NotifyAutoscrollHandledByAPZ(const FrameMetrics::ViewID& aScrollId) override;
 private:
   nsCOMPtr<nsIWidget> mWidget;
   RefPtr<APZEventState> mAPZEventState;
   RefPtr<IAPZCTreeManager> mAPZCTreeManager;
   MessageLoop* mUILoop;
 
   void InitializeRoot();
   nsIPresShell* GetPresShell() const;
--- a/gfx/layers/apz/util/ContentProcessController.cpp
+++ b/gfx/layers/apz/util/ContentProcessController.cpp
@@ -88,16 +88,22 @@ ContentProcessController::NotifyFlushCom
 
 void
 ContentProcessController::NotifyAsyncScrollbarDragRejected(const FrameMetrics::ViewID& aScrollId)
 {
   APZCCallbackHelper::NotifyAsyncScrollbarDragRejected(aScrollId);
 }
 
 void
+ContentProcessController::NotifyAutoscrollHandledByAPZ(const FrameMetrics::ViewID& aScrollId)
+{
+  APZCCallbackHelper::NotifyAutoscrollHandledByAPZ(aScrollId);
+}
+
+void
 ContentProcessController::PostDelayedTask(already_AddRefed<Runnable> aRunnable, int aDelayMs)
 {
   MOZ_ASSERT_UNREACHABLE("ContentProcessController should only be used remotely.");
 }
 
 bool
 ContentProcessController::IsRepaintThread()
 {
--- a/gfx/layers/apz/util/ContentProcessController.h
+++ b/gfx/layers/apz/util/ContentProcessController.h
@@ -59,16 +59,18 @@ public:
 
   void NotifyMozMouseScrollEvent(const FrameMetrics::ViewID& aScrollId,
                                  const nsString& aEvent) override;
 
   void NotifyFlushComplete() override;
 
   void NotifyAsyncScrollbarDragRejected(const FrameMetrics::ViewID& aScrollId) override;
 
+  void NotifyAutoscrollHandledByAPZ(const FrameMetrics::ViewID& aScrollId) override;
+
   void PostDelayedTask(already_AddRefed<Runnable> aRunnable, int aDelayMs) override;
 
   bool IsRepaintThread() override;
 
   void DispatchToRepaintThread(already_AddRefed<Runnable> aTask) override;
 
 private:
   RefPtr<dom::TabChild> mBrowser;
--- a/gfx/layers/apz/util/ScrollInputMethods.h
+++ b/gfx/layers/apz/util/ScrollInputMethods.h
@@ -46,26 +46,31 @@ enum class ScrollInputMethod {
   MainThreadScrollbarButtonClick,  // clicking the buttons at the ends of the
                                    // scrollback track
   MainThreadScrollbarTrackClick,   // clicking the scrollbar track above or
                                    // below the thumb
 
   // Autoscrolling
   MainThreadAutoscrolling,    // autoscrolling
 
+  // === Additional input methods implemented in APZ ===
+
   // Async Keyboard
   ApzScrollLine,       // line scrolling
                        // (generally triggered by up/down arrow keys)
   ApzScrollCharacter,  // character scrolling
                        // (generally triggered by left/right arrow keys)
   ApzScrollPage,       // page scrolling
                        // (generally triggered by PageUp/PageDown keys)
   ApzCompleteScroll,   // scrolling to the end of the scroll range
                        // (generally triggered by Home/End keys)
 
+  // Autoscrolling
+  ApzAutoscrolling,
+
   // New input methods can be added at the end, up to a maximum of 64.
   // They should only be added at the end, to preserve the numerical values
   // of the existing enumerators.
 };
 
 } // namespace layers
 } // namespace mozilla
 
--- a/gfx/layers/ipc/APZCTreeManagerChild.cpp
+++ b/gfx/layers/ipc/APZCTreeManagerChild.cpp
@@ -203,16 +203,30 @@ void
 APZCTreeManagerChild::StartScrollbarDrag(
     const ScrollableLayerGuid& aGuid,
     const AsyncDragMetrics& aDragMetrics)
 {
   SendStartScrollbarDrag(aGuid, aDragMetrics);
 }
 
 void
+APZCTreeManagerChild::StartAutoscroll(
+    const ScrollableLayerGuid& aGuid,
+    const ScreenPoint& aAnchorLocation)
+{
+  SendStartAutoscroll(aGuid, aAnchorLocation);
+}
+
+void
+APZCTreeManagerChild::StopAutoscroll(const ScrollableLayerGuid& aGuid)
+{
+  SendStopAutoscroll(aGuid);
+}
+
+void
 APZCTreeManagerChild::SetLongTapEnabled(bool aTapGestureEnabled)
 {
   SendSetLongTapEnabled(aTapGestureEnabled);
 }
 
 void
 APZCTreeManagerChild::ProcessTouchVelocity(uint32_t aTimestampMs, float aSpeedY)
 {
--- a/gfx/layers/ipc/APZCTreeManagerChild.h
+++ b/gfx/layers/ipc/APZCTreeManagerChild.h
@@ -66,16 +66,24 @@ public:
           const nsTArray<TouchBehaviorFlags>& aValues) override;
 
   void
   StartScrollbarDrag(
           const ScrollableLayerGuid& aGuid,
           const AsyncDragMetrics& aDragMetrics) override;
 
   void
+  StartAutoscroll(
+          const ScrollableLayerGuid& aGuid,
+          const ScreenPoint& aAnchorLocation) override;
+
+  void
+  StopAutoscroll(const ScrollableLayerGuid& aGuid) override;
+
+  void
   SetLongTapEnabled(bool aTapGestureEnabled) override;
 
   void
   ProcessTouchVelocity(uint32_t aTimestampMs, float aSpeedY) override;
 
   void
   ProcessUnhandledEvent(
           LayoutDeviceIntPoint* aRefPoint,
--- a/gfx/layers/ipc/APZCTreeManagerParent.cpp
+++ b/gfx/layers/ipc/APZCTreeManagerParent.cpp
@@ -296,16 +296,53 @@ APZCTreeManagerParent::RecvStartScrollba
       &IAPZCTreeManager::StartScrollbarDrag,
       aGuid,
       aDragMetrics));
 
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
+APZCTreeManagerParent::RecvStartAutoscroll(
+    const ScrollableLayerGuid& aGuid,
+    const ScreenPoint& aAnchorLocation)
+{
+  // Unlike RecvStartScrollbarDrag(), this message comes from the parent
+  // process (via nsBaseWidget::mAPZC) rather than from the child process
+  // (via TabChild::mApzcTreeManager), so there is no need to check the
+  // layers id against mLayersId (and in any case, it wouldn't match, because
+  // mLayersId stores the parent process's layers id, while nsBaseWidget is
+  // sending the child process's layers id).
+
+  APZThreadUtils::RunOnControllerThread(
+      NewRunnableMethod<ScrollableLayerGuid, ScreenPoint>(
+        "layers::IAPZCTreeManager::StartAutoscroll",
+        mTreeManager,
+        &IAPZCTreeManager::StartAutoscroll,
+        aGuid, aAnchorLocation));
+
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+APZCTreeManagerParent::RecvStopAutoscroll(const ScrollableLayerGuid& aGuid)
+{
+  // See RecvStartAutoscroll() for why we don't check the layers id.
+
+  APZThreadUtils::RunOnControllerThread(
+      NewRunnableMethod<ScrollableLayerGuid>(
+        "layers::IAPZCTreeManager::StopAutoscroll",
+        mTreeManager,
+        &IAPZCTreeManager::StopAutoscroll,
+        aGuid));
+
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
 APZCTreeManagerParent::RecvSetLongTapEnabled(const bool& aTapGestureEnabled)
 {
   mTreeManager->SetLongTapEnabled(aTapGestureEnabled);
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
 APZCTreeManagerParent::RecvProcessTouchVelocity(
--- a/gfx/layers/ipc/APZCTreeManagerParent.h
+++ b/gfx/layers/ipc/APZCTreeManagerParent.h
@@ -122,16 +122,24 @@ public:
           nsTArray<TouchBehaviorFlags>&& aValues) override;
 
   mozilla::ipc::IPCResult
   RecvStartScrollbarDrag(
           const ScrollableLayerGuid& aGuid,
           const AsyncDragMetrics& aDragMetrics) override;
 
   mozilla::ipc::IPCResult
+  RecvStartAutoscroll(
+          const ScrollableLayerGuid& aGuid,
+          const ScreenPoint& aAnchorLocation) override;
+
+  mozilla::ipc::IPCResult
+  RecvStopAutoscroll(const ScrollableLayerGuid& aGuid) override;
+
+  mozilla::ipc::IPCResult
   RecvSetLongTapEnabled(const bool& aTapGestureEnabled) override;
 
   mozilla::ipc::IPCResult
   RecvProcessTouchVelocity(
           const uint32_t& aTimestampMs,
           const float& aSpeedY) override;
 
   mozilla::ipc::IPCResult
--- a/gfx/layers/ipc/APZChild.cpp
+++ b/gfx/layers/ipc/APZChild.cpp
@@ -81,16 +81,23 @@ APZChild::RecvNotifyFlushComplete()
 mozilla::ipc::IPCResult
 APZChild::RecvNotifyAsyncScrollbarDragRejected(const ViewID& aScrollId)
 {
   mController->NotifyAsyncScrollbarDragRejected(aScrollId);
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
+APZChild::RecvNotifyAutoscrollHandledByAPZ(const ViewID& aScrollId)
+{
+  mController->NotifyAutoscrollHandledByAPZ(aScrollId);
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
 APZChild::RecvDestroy()
 {
   // mController->Destroy will be called in the destructor
   PAPZChild::Send__delete__(this);
   return IPC_OK();
 }
 
 
--- a/gfx/layers/ipc/APZChild.h
+++ b/gfx/layers/ipc/APZChild.h
@@ -37,16 +37,18 @@ public:
   mozilla::ipc::IPCResult RecvNotifyAPZStateChange(const ScrollableLayerGuid& aGuid,
                                                    const APZStateChange& aChange,
                                                    const int& aArg) override;
 
   mozilla::ipc::IPCResult RecvNotifyFlushComplete() override;
 
   mozilla::ipc::IPCResult RecvNotifyAsyncScrollbarDragRejected(const ViewID& aScrollId) override;
 
+  mozilla::ipc::IPCResult RecvNotifyAutoscrollHandledByAPZ(const ViewID& aScrollId) override;
+
   mozilla::ipc::IPCResult RecvDestroy() override;
 
 private:
   RefPtr<GeckoContentController> mController;
 };
 
 } // namespace layers
 
--- a/gfx/layers/ipc/CompositorManagerChild.cpp
+++ b/gfx/layers/ipc/CompositorManagerChild.cpp
@@ -1,26 +1,31 @@
 /* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/layers/CompositorManagerChild.h"
+
+#include "gfxPrefs.h"
 #include "mozilla/layers/CompositorBridgeChild.h"
 #include "mozilla/layers/CompositorManagerParent.h"
 #include "mozilla/layers/CompositorThread.h"
+#include "mozilla/gfx/gfxVars.h"
 #include "mozilla/gfx/GPUProcessManager.h"
 #include "mozilla/dom/ContentChild.h"   // for ContentChild
 #include "mozilla/dom/TabChild.h"       // for TabChild
 #include "mozilla/dom/TabGroup.h"       // for TabGroup
 #include "VsyncSource.h"
 
 namespace mozilla {
 namespace layers {
 
+using gfx::GPUProcessManager;
+
 StaticRefPtr<CompositorManagerChild> CompositorManagerChild::sInstance;
 
 /* static */ bool
 CompositorManagerChild::IsInitialized(base::ProcessId aGPUPid)
 {
   MOZ_ASSERT(NS_IsMainThread());
   return sInstance && sInstance->CanSend() && sInstance->OtherPid() == aGPUPid;
 }
@@ -260,17 +265,17 @@ CompositorManagerChild::GetSpecificMessa
 void
 CompositorManagerChild::SetReplyTimeout()
 {
 #ifndef DEBUG
   // Add a timeout for release builds to kill GPU process when it hangs.
   // Don't apply timeout when using web render as it tend to timeout frequently.
   if (XRE_IsParentProcess() &&
       GPUProcessManager::Get()->GetGPUChild() &&
-      !gfxVars::UseWebRender()) {
+      !gfx::gfxVars::UseWebRender()) {
     int32_t timeout = gfxPrefs::GPUProcessIPCReplyTimeoutMs();
     SetReplyTimeoutMs(timeout);
   }
 #endif
 }
 
 bool
 CompositorManagerChild::ShouldContinueFromReplyTimeout()
--- a/gfx/layers/ipc/PAPZ.ipdl
+++ b/gfx/layers/ipc/PAPZ.ipdl
@@ -60,13 +60,15 @@ child:
   async NotifyMozMouseScrollEvent(ViewID aScrollId, nsString aEvent);
 
   async NotifyAPZStateChange(ScrollableLayerGuid aGuid, APZStateChange aChange, int aArg);
 
   async NotifyFlushComplete();
 
   async NotifyAsyncScrollbarDragRejected(ViewID aScrollId);
 
+  async NotifyAutoscrollHandledByAPZ(ViewID aScrollId);
+
   async Destroy();
 };
 
 } // layers
 } // mozilla
--- a/gfx/layers/ipc/PAPZCTreeManager.ipdl
+++ b/gfx/layers/ipc/PAPZCTreeManager.ipdl
@@ -5,16 +5,17 @@
 
 include "mozilla/GfxMessageUtils.h";
 include "mozilla/layers/LayersMessageUtils.h";
 include "ipc/nsGUIEventIPC.h";
 include "mozilla/dom/TabMessageUtils.h"; // Needed for IPC::ParamTraits<nsEventStatus>.
 
 include protocol PCompositorBridge;
 
+using CSSPoint from "Units.h";
 using CSSRect from "Units.h";
 using LayoutDeviceCoord from "Units.h";
 using LayoutDeviceIntPoint from "Units.h";
 using mozilla::LayoutDevicePoint from "Units.h";
 using ScreenPoint from "Units.h";
 using struct mozilla::layers::ScrollableLayerGuid from "FrameMetrics.h";
 using mozilla::layers::MaybeZoomConstraints from "FrameMetrics.h";
 using mozilla::layers::TouchBehaviorFlags from "mozilla/layers/APZUtils.h";
@@ -74,16 +75,20 @@ parent:
   async SetKeyboardMap(KeyboardMap aKeyboardMap);
 
   async SetDPI(float aDpiValue);
 
   async SetAllowedTouchBehavior(uint64_t aInputBlockId, TouchBehaviorFlags[] aValues);
 
   async StartScrollbarDrag(ScrollableLayerGuid aGuid, AsyncDragMetrics aDragMetrics);
 
+  async StartAutoscroll(ScrollableLayerGuid aGuid, ScreenPoint aAnchorLocation);
+
+  async StopAutoscroll(ScrollableLayerGuid aGuid);
+
   async SetLongTapEnabled(bool aTapGestureEnabled);
 
   async ProcessTouchVelocity(uint32_t aTimestampMs, float aSpeedY);
 
   // The following messages are used to
   // implement the ReceiveInputEvent methods
 
   sync ReceiveMultiTouchInputEvent(MultiTouchInput aEvent)
--- a/gfx/layers/ipc/RemoteContentController.cpp
+++ b/gfx/layers/ipc/RemoteContentController.cpp
@@ -267,16 +267,34 @@ RemoteContentController::NotifyAsyncScro
   }
 
   if (mCanSend) {
     Unused << SendNotifyAsyncScrollbarDragRejected(aScrollId);
   }
 }
 
 void
+RemoteContentController::NotifyAutoscrollHandledByAPZ(const FrameMetrics::ViewID& aScrollId)
+{
+  if (MessageLoop::current() != mCompositorThread) {
+    // We have to send messages from the compositor thread
+    mCompositorThread->PostTask(NewRunnableMethod<FrameMetrics::ViewID>(
+      "layers::RemoteContentController::NotifyAutoscrollHandledByAPZ",
+      this,
+      &RemoteContentController::NotifyAutoscrollHandledByAPZ,
+      aScrollId));
+    return;
+  }
+
+  if (mCanSend) {
+    Unused << SendNotifyAutoscrollHandledByAPZ(aScrollId);
+  }
+}
+
+void
 RemoteContentController::ActorDestroy(ActorDestroyReason aWhy)
 {
   // This controller could possibly be kept alive longer after this
   // by a RefPtr, but it is no longer valid to send messages.
   mCanSend = false;
 }
 
 void
--- a/gfx/layers/ipc/RemoteContentController.h
+++ b/gfx/layers/ipc/RemoteContentController.h
@@ -69,16 +69,18 @@ public:
 
   virtual void NotifyMozMouseScrollEvent(const FrameMetrics::ViewID& aScrollId,
                                          const nsString& aEvent) override;
 
   virtual void NotifyFlushComplete() override;
 
   virtual void NotifyAsyncScrollbarDragRejected(const FrameMetrics::ViewID& aScrollId) override;
 
+  virtual void NotifyAutoscrollHandledByAPZ(const FrameMetrics::ViewID& aScrollId) override;
+
   virtual void ActorDestroy(ActorDestroyReason aWhy) override;
 
   virtual void Destroy() override;
 
 private:
   MessageLoop* mCompositorThread;
   bool mCanSend;
 
--- a/gfx/layers/mlgpu/BufferCache.h
+++ b/gfx/layers/mlgpu/BufferCache.h
@@ -2,16 +2,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 mozilla_gfx_layers_mlgpu_BufferCache_h
 #define mozilla_gfx_layers_mlgpu_BufferCache_h
 
 #include "mozilla/EnumeratedArray.h"
+#include "mozilla/RefPtr.h"
 #include <deque>
 #include <vector>
 
 namespace mozilla {
 namespace layers {
 
 class MLGBuffer;
 class MLGDevice;
--- a/gfx/layers/mlgpu/RenderViewMLGPU.cpp
+++ b/gfx/layers/mlgpu/RenderViewMLGPU.cpp
@@ -7,16 +7,18 @@
 #include "ContainerLayerMLGPU.h"
 #include "FrameBuilder.h"
 #include "gfxPrefs.h"
 #include "LayersHelpers.h"
 #include "LayersLogging.h"
 #include "MLGDevice.h"
 #include "RenderPassMLGPU.h"
 #include "ShaderDefinitionsMLGPU.h"
+#include "Units.h"
+#include "UnitTransforms.h"
 #include "UtilityMLGPU.h"
 
 namespace mozilla {
 namespace layers {
 
 using namespace gfx;
 
 RenderViewMLGPU::RenderViewMLGPU(FrameBuilder* aBuilder,
--- a/gfx/layers/mlgpu/SharedBufferMLGPU.cpp
+++ b/gfx/layers/mlgpu/SharedBufferMLGPU.cpp
@@ -153,28 +153,138 @@ SharedBufferMLGPU::Unmap()
 
   mBytesUsedThisFrame += mCurrentPosition;
 
   mDevice->Unmap(mBuffer);
   mMap = MLGMappedResource();
   mMapped = false;
 }
 
+uint8_t*
+SharedBufferMLGPU::GetBufferPointer(size_t aBytes, ptrdiff_t* aOutOffset, RefPtr<MLGBuffer>* aOutBuffer)
+{
+  if (!EnsureMappedBuffer(aBytes)) {
+    return nullptr;
+  }
+
+  ptrdiff_t newPos = mCurrentPosition + aBytes;
+  MOZ_ASSERT(size_t(newPos) <= mMaxSize);
+
+  *aOutOffset = mCurrentPosition;
+  *aOutBuffer = mBuffer;
+
+  uint8_t* ptr = reinterpret_cast<uint8_t*>(mMap.mData) + mCurrentPosition;
+  mCurrentPosition = newPos;
+  return ptr;
+}
+
+VertexBufferSection::VertexBufferSection()
+ : mOffset(-1),
+   mNumVertices(0),
+   mStride(0)
+{}
+
+void
+VertexBufferSection::Init(MLGBuffer* aBuffer, ptrdiff_t aOffset, size_t aNumVertices, size_t aStride)
+{
+  mBuffer = aBuffer;
+  mOffset = aOffset;
+  mNumVertices = aNumVertices;
+  mStride = aStride;
+}
+
+ConstantBufferSection::ConstantBufferSection()
+ : mOffset(-1)
+{}
+
+void
+ConstantBufferSection::Init(MLGBuffer* aBuffer, ptrdiff_t aOffset, size_t aBytes, size_t aNumItems)
+{
+  mBuffer = aBuffer;
+  mOffset = aOffset;
+  mNumBytes = aBytes;
+  mNumItems = aNumItems;
+}
+
 SharedVertexBuffer::SharedVertexBuffer(MLGDevice* aDevice, size_t aDefaultSize)
  : SharedBufferMLGPU(aDevice, MLGBufferType::Vertex, aDefaultSize)
 {
 }
 
+bool
+SharedVertexBuffer::Allocate(VertexBufferSection* aHolder,
+                             size_t aNumItems,
+                             size_t aSizeOfItem,
+                             const void* aData)
+{
+  RefPtr<MLGBuffer> buffer;
+  ptrdiff_t offset;
+  size_t bytes = aSizeOfItem * aNumItems;
+  uint8_t* ptr = GetBufferPointer(bytes, &offset, &buffer);
+  if (!ptr) {
+    return false;
+  }
+
+  memcpy(ptr, aData, bytes);
+  aHolder->Init(buffer, offset, aNumItems, aSizeOfItem);
+  return true;
+}
+
+AutoBufferUploadBase::AutoBufferUploadBase()
+  : mPtr(nullptr)
+{
+}
+
+AutoBufferUploadBase::~AutoBufferUploadBase()
+{
+  if (mBuffer) {
+    UnmapBuffer();
+  }
+}
+
+void
+AutoBufferUploadBase::Init(void* aPtr, MLGDevice* aDevice, MLGBuffer* aBuffer)
+{
+  MOZ_ASSERT(!mPtr && aPtr);
+  mPtr = aPtr;
+  mDevice = aDevice;
+  mBuffer = aBuffer;
+}
+
 SharedConstantBuffer::SharedConstantBuffer(MLGDevice* aDevice, size_t aDefaultSize)
  : SharedBufferMLGPU(aDevice, MLGBufferType::Constant, aDefaultSize)
 {
   mMaxConstantBufferBindSize = aDevice->GetMaxConstantBufferBindSize();
   mCanUseOffsetAllocation = aDevice->CanUseConstantBufferOffsetBinding();
 }
 
+bool
+SharedConstantBuffer::Allocate(ConstantBufferSection* aHolder,
+                               AutoBufferUploadBase* aPtr,
+                               size_t aNumItems,
+                               size_t aSizeOfItem)
+{
+  MOZ_ASSERT(aSizeOfItem % 16 == 0, "Items must be padded to 16 bytes");
+
+  size_t bytes = aNumItems * aSizeOfItem;
+  if (bytes > mMaxConstantBufferBindSize) {
+    gfxWarning() << "Attempted to allocate too many bytes into a constant buffer";
+    return false;
+  }
+
+  RefPtr<MLGBuffer> buffer;
+  ptrdiff_t offset;
+  if (!GetBufferPointer(aPtr, bytes, &offset, &buffer)) {
+    return false;
+  }
+
+  aHolder->Init(buffer, offset, bytes, aNumItems);
+  return true;
+}
+
 uint8_t*
 SharedConstantBuffer::AllocateNewBuffer(size_t aBytes, ptrdiff_t* aOutOffset, RefPtr<MLGBuffer>* aOutBuffer)
 {
   RefPtr<MLGBuffer> buffer;
   if (BufferCache* cache = mDevice->GetConstantBufferCache()) {
     buffer = cache->GetOrCreateBuffer(aBytes);
   } else {
     buffer = mDevice->CreateBuffer(MLGBufferType::Constant, aBytes, MLGUsage::Dynamic);
--- a/gfx/layers/mlgpu/SharedBufferMLGPU.h
+++ b/gfx/layers/mlgpu/SharedBufferMLGPU.h
@@ -33,31 +33,17 @@ protected:
   SharedBufferMLGPU(MLGDevice* aDevice, MLGBufferType aType, size_t aDefaultSize);
 
   bool EnsureMappedBuffer(size_t aBytes);
   bool GrowBuffer(size_t aBytes);
   void ForgetBuffer();
   bool Map();
   void Unmap();
 
-  uint8_t* GetBufferPointer(size_t aBytes, ptrdiff_t* aOutOffset, RefPtr<MLGBuffer>* aOutBuffer) {
-    if (!EnsureMappedBuffer(aBytes)) {
-      return nullptr;
-    }
-
-    ptrdiff_t newPos = mCurrentPosition + aBytes;
-    MOZ_ASSERT(size_t(newPos) <= mMaxSize);
-
-    *aOutOffset = mCurrentPosition;
-    *aOutBuffer = mBuffer;
-
-    uint8_t* ptr = reinterpret_cast<uint8_t*>(mMap.mData) + mCurrentPosition;
-    mCurrentPosition = newPos;
-    return ptr;
-  }
+  uint8_t* GetBufferPointer(size_t aBytes, ptrdiff_t* aOutOffset, RefPtr<MLGBuffer>* aOutBuffer);
 
 protected:
   // Note: RefPtr here would cause a cycle. Only MLGDevice should own
   // SharedBufferMLGPU objects for now.
   MLGDevice* mDevice;
   MLGBufferType mType;
   size_t mDefaultSize;
   bool mCanUseOffsetAllocation;
@@ -76,21 +62,17 @@ protected:
   size_t mBytesUsedThisFrame;
   size_t mNumSmallFrames;
 };
 
 class VertexBufferSection final
 {
   friend class SharedVertexBuffer;
 public:
-  VertexBufferSection()
-   : mOffset(-1),
-     mNumVertices(0),
-     mStride(0)
-  {}
+  VertexBufferSection();
 
   uint32_t Stride() const {
     return mStride;
   }
   MLGBuffer* GetBuffer() const {
     return mBuffer;
   }
   ptrdiff_t Offset() const {
@@ -100,38 +82,31 @@ public:
   size_t NumVertices() const {
     return mNumVertices;
   }
   bool IsValid() const {
     return !!mBuffer;
   }
 
 protected:
-  void Init(MLGBuffer* aBuffer, ptrdiff_t aOffset, size_t aNumVertices, size_t aStride) {
-    mBuffer = aBuffer;
-    mOffset = aOffset;
-    mNumVertices = aNumVertices;
-    mStride = aStride;
-  }
+  void Init(MLGBuffer* aBuffer, ptrdiff_t aOffset, size_t aNumVertices, size_t aStride);
 
 protected:
   RefPtr<MLGBuffer> mBuffer;
   ptrdiff_t mOffset;
   size_t mNumVertices;
   size_t mStride;
 };
 
 class ConstantBufferSection final
 {
   friend class SharedConstantBuffer;
 
 public:
-  ConstantBufferSection()
-   : mOffset(-1)
-  {}
+  ConstantBufferSection();
 
   uint32_t NumConstants() const {
     return NumConstantsForBytes(mNumBytes);
   }
   size_t NumItems() const {
     return mNumItems;
   }
   uint32_t Offset() const {
@@ -148,22 +123,17 @@ public:
     return mOffset != -1;
   }
 
 protected:
   static constexpr size_t NumConstantsForBytes(size_t aBytes) {
     return (aBytes + ((256 - (aBytes % 256)) % 256)) / 16;
   }
 
-  void Init(MLGBuffer* aBuffer, ptrdiff_t aOffset, size_t aBytes, size_t aNumItems) {
-    mBuffer = aBuffer;
-    mOffset = aOffset;
-    mNumBytes = aBytes;
-    mNumItems = aNumItems;
-  }
+  void Init(MLGBuffer* aBuffer, ptrdiff_t aOffset, size_t aBytes, size_t aNumItems);
 
 protected:
   RefPtr<MLGBuffer> mBuffer;
   ptrdiff_t mOffset;
   size_t mNumBytes;
   size_t mNumItems;
 };
 
@@ -184,62 +154,40 @@ public:
   }
 
   // Allocate a buffer that can be uploaded immediately. This is the
   // direct access version, for cases where a StagingBuffer is not
   // needed.
   bool Allocate(VertexBufferSection* aHolder,
                 size_t aNumItems,
                 size_t aSizeOfItem,
-                const void* aData)
-  {
-    RefPtr<MLGBuffer> buffer;
-    ptrdiff_t offset;
-    size_t bytes = aSizeOfItem * aNumItems;
-    uint8_t* ptr = GetBufferPointer(bytes, &offset, &buffer);
-    if (!ptr) {
-      return false;
-    }
-
-    memcpy(ptr, aData, bytes);
-    aHolder->Init(buffer, offset, aNumItems, aSizeOfItem);
-    return true;
-  }
+                const void* aData);
 
   template <typename T>
   bool Allocate(VertexBufferSection* aHolder, const T& aItem) {
     return Allocate(aHolder, 1, sizeof(T), &aItem);
   }
 };
 
 // To support older Direct3D versions, we need to support one-off MLGBuffers,
 // where data is uploaded immediately rather than at the end of all batch
 // preparation. We achieve this through a small helper class.
 //
 // Note: the unmap is not inline sincce we don't include MLGDevice.h.
 class MOZ_STACK_CLASS AutoBufferUploadBase
 {
 public:
-  AutoBufferUploadBase() : mPtr(nullptr) {}
-  ~AutoBufferUploadBase() {
-    if (mBuffer) {
-      UnmapBuffer();
-    }
-  }
+  AutoBufferUploadBase();
+  ~AutoBufferUploadBase();
 
   void Init(void* aPtr) {
     MOZ_ASSERT(!mPtr && aPtr);
     mPtr = aPtr;
   }
-  void Init(void* aPtr, MLGDevice* aDevice, MLGBuffer* aBuffer) {
-    MOZ_ASSERT(!mPtr && aPtr);
-    mPtr = aPtr;
-    mDevice = aDevice;
-    mBuffer = aBuffer;
-  }
+  void Init(void* aPtr, MLGDevice* aDevice, MLGBuffer* aBuffer);
   void* get() {
     return const_cast<void*>(mPtr);
   }
 
 private:
   void UnmapBuffer();
 
 protected:
@@ -307,36 +255,17 @@ private:
     }
     memcpy(ptr.get(), aData, aNumItems * aSizeOfItem);
     return true;
   }
 
   bool Allocate(ConstantBufferSection* aHolder,
                 AutoBufferUploadBase* aPtr,
                 size_t aNumItems,
-                size_t aSizeOfItem)
-  {
-    MOZ_ASSERT(aSizeOfItem % 16 == 0, "Items must be padded to 16 bytes");
-
-    size_t bytes = aNumItems * aSizeOfItem;
-    if (bytes > mMaxConstantBufferBindSize) {
-      gfxWarning() << "Attempted to allocate too many bytes into a constant buffer";
-      return false;
-    }
-
-
-    RefPtr<MLGBuffer> buffer;
-    ptrdiff_t offset;
-    if (!GetBufferPointer(aPtr, bytes, &offset, &buffer)) {
-      return false;
-    }
-
-    aHolder->Init(buffer, offset, bytes, aNumItems);
-    return true;
-  }
+                size_t aSizeOfItem);
 
   bool GetBufferPointer(AutoBufferUploadBase* aPtr,
                         size_t aBytes,
                         ptrdiff_t* aOutOffset,
                         RefPtr<MLGBuffer>* aOutBuffer)
   {
     if (!mCanUseOffsetAllocation) {
       uint8_t* ptr = AllocateNewBuffer(aBytes, aOutOffset, aOutBuffer);
--- a/gfx/layers/moz.build
+++ b/gfx/layers/moz.build
@@ -286,16 +286,17 @@ if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'andr
     ]
 
 UNIFIED_SOURCES += [
     'AnimationHelper.cpp',
     'AnimationInfo.cpp',
     'apz/public/IAPZCTreeManager.cpp',
     'apz/src/APZCTreeManager.cpp',
     'apz/src/AsyncPanZoomController.cpp',
+    'apz/src/AutoscrollAnimation.cpp',
     'apz/src/Axis.cpp',
     'apz/src/CheckerboardEvent.cpp',
     'apz/src/DragTracker.cpp',
     'apz/src/FocusState.cpp',
     'apz/src/FocusTarget.cpp',
     'apz/src/GenericScrollAnimation.cpp',
     'apz/src/GestureEventListener.cpp',
     'apz/src/HitTestingTreeNode.cpp',
--- a/gfx/layers/wr/WebRenderBridgeParent.cpp
+++ b/gfx/layers/wr/WebRenderBridgeParent.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 "mozilla/layers/WebRenderBridgeParent.h"
 
 #include "apz/src/AsyncPanZoomController.h"
 #include "CompositableHost.h"
+#include "gfxEnv.h"
 #include "gfxPrefs.h"
 #include "gfxEnv.h"
 #include "GeckoProfiler.h"
 #include "GLContext.h"
 #include "GLContextProvider.h"
 #include "mozilla/Range.h"
 #include "mozilla/layers/AnimationHelper.h"
 #include "mozilla/layers/APZCTreeManager.h"
--- a/gfx/layers/wr/WebRenderLayerManager.cpp
+++ b/gfx/layers/wr/WebRenderLayerManager.cpp
@@ -232,47 +232,61 @@ WebRenderLayerManager::CreateWebRenderCo
     if (item->ShouldFlattenAway(aDisplayListBuilder)) {
       aDisplayList->AppendToBottom(itemSameCoordinateSystemChildren);
       item->~nsDisplayItem();
       continue;
     }
 
     savedItems.AppendToTop(item);
 
+    bool forceNewLayerData = false;
+    size_t layerCountBeforeRecursing = mLayerScrollData.size();
     if (apzEnabled) {
-      bool forceNewLayerData = false;
-
       // For some types of display items we want to force a new
       // WebRenderLayerScrollData object, to ensure we preserve the APZ-relevant
       // data that is in the display item.
-      switch (itemType) {
-      case nsDisplayItem::TYPE_SCROLL_INFO_LAYER:
-      case nsDisplayItem::TYPE_REMOTE:
-        forceNewLayerData = true;
-        break;
-      default:
-        break;
-      }
+      forceNewLayerData = item->UpdateScrollData(nullptr, nullptr);
 
       // Anytime the ASR changes we also want to force a new layer data because
       // the stack of scroll metadata is going to be different for this
       // display item than previously, so we can't squash the display items
       // into the same "layer".
       const ActiveScrolledRoot* asr = item->GetActiveScrolledRoot();
-      if (forceNewLayerData || asr != lastAsr) {
+      if (asr != lastAsr) {
         lastAsr = asr;
-        mLayerScrollData.emplace_back();
-        mLayerScrollData.back().Initialize(mScrollData, item);
+        forceNewLayerData = true;
+      }
+
+      // If we're going to create a new layer data for this item, stash the
+      // ASR so that if we recurse into a sublist they will know where to stop
+      // walking up their ASR chain when building scroll metadata.
+      if (forceNewLayerData) {
+        mAsrStack.push_back(asr);
       }
     }
 
+    // Note: this call to CreateWebRenderCommands can recurse back into
+    // this function if the |item| is a wrapper for a sublist.
     if (!item->CreateWebRenderCommands(aBuilder, aSc, mParentCommands, this,
                                        aDisplayListBuilder)) {
       PushItemAsImage(item, aBuilder, aSc, aDisplayListBuilder);
     }
+
+    if (apzEnabled && forceNewLayerData) {
+      // Pop the thing we pushed before the recursion, so the topmost item on
+      // the stack is enclosing display item's ASR (or the stack is empty)
+      mAsrStack.pop_back();
+      const ActiveScrolledRoot* stopAtAsr =
+          mAsrStack.empty() ? nullptr : mAsrStack.back();
+
+      int32_t descendants = mLayerScrollData.size() - layerCountBeforeRecursing;
+
+      mLayerScrollData.emplace_back();
+      mLayerScrollData.back().Initialize(mScrollData, item, descendants, stopAtAsr);
+    }
   }
   aDisplayList->AppendToTop(&savedItems);
 }
 
 void
 WebRenderLayerManager::EndTransactionWithoutLayer(nsDisplayList* aDisplayList,
                                                   nsDisplayListBuilder* aDisplayListBuilder)
 {
--- a/gfx/layers/wr/WebRenderLayerManager.h
+++ b/gfx/layers/wr/WebRenderLayerManager.h
@@ -19,16 +19,19 @@
 #include "mozilla/layers/WebRenderUserData.h"
 #include "mozilla/webrender/WebRenderAPI.h"
 #include "mozilla/webrender/WebRenderTypes.h"
 #include "nsDisplayList.h"
 
 class nsIWidget;
 
 namespace mozilla {
+
+struct ActiveScrolledRoot;
+
 namespace layers {
 
 class CompositorBridgeChild;
 class KnowsCompositor;
 class PCompositorBridgeChild;
 class WebRenderBridgeChild;
 class WebRenderParentCommand;
 
@@ -240,16 +243,21 @@ private:
   nsTArray<WebRenderParentCommand> mParentCommands;
 
   // This holds the scroll data that we need to send to the compositor for
   // APZ to do it's job
   WebRenderScrollData mScrollData;
   // We use this as a temporary data structure while building the mScrollData
   // inside a layers-free transaction.
   std::vector<WebRenderLayerScrollData> mLayerScrollData;
+  // We use this as a temporary data structure to track the current display
+  // item's ASR as we recurse in CreateWebRenderCommandsFromDisplayList. We
+  // need this so that WebRenderLayerScrollData items that deeper in the
+  // tree don't duplicate scroll metadata that their ancestors already have.
+  std::vector<const ActiveScrolledRoot*> mAsrStack;
 
   // Layers that have been mutated. If we have an empty transaction
   // then a display item layer will no longer be valid
   // if it was a mutated layers.
   void AddMutatedLayer(Layer* aLayer);
   void ClearMutatedLayers();
   LayerRefArray mMutatedLayers;
   bool mTransactionIncomplete;
--- a/gfx/layers/wr/WebRenderScrollData.cpp
+++ b/gfx/layers/wr/WebRenderScrollData.cpp
@@ -64,41 +64,28 @@ WebRenderLayerScrollData::Initialize(Web
 void
 WebRenderLayerScrollData::InitializeRoot(int32_t aDescendantCount)
 {
   mDescendantCount = aDescendantCount;
 }
 
 void
 WebRenderLayerScrollData::Initialize(WebRenderScrollData& aOwner,
-                                     nsDisplayItem* aItem)
+                                     nsDisplayItem* aItem,
+                                     int32_t aDescendantCount,
+                                     const ActiveScrolledRoot* aStopAtAsr)
 {
-  mDescendantCount = 0;
+  MOZ_ASSERT(aDescendantCount >= 0); // Ensure value is valid
+  MOZ_ASSERT(mDescendantCount == -1); // Don't allow re-setting an already set value
+  mDescendantCount = aDescendantCount;
 
   MOZ_ASSERT(aItem);
-  switch (aItem->GetType()) {
-  case nsDisplayItem::TYPE_SCROLL_INFO_LAYER: {
-    nsDisplayScrollInfoLayer* info = static_cast<nsDisplayScrollInfoLayer*>(aItem);
-    UniquePtr<ScrollMetadata> metadata = info->ComputeScrollMetadata(
-        nullptr, ContainerLayerParameters());
-    MOZ_ASSERT(metadata);
-    MOZ_ASSERT(metadata->GetMetrics().IsScrollInfoLayer());
-    mScrollIds.AppendElement(aOwner.AddMetadata(*metadata));
-    break;
-  }
-  case nsDisplayItem::TYPE_REMOTE: {
-    nsDisplayRemote* remote = static_cast<nsDisplayRemote*>(aItem);
-    mReferentId = Some(remote->GetRemoteLayersId());
-    break;
-  }
-  default:
-    break;
-  }
+  aItem->UpdateScrollData(&aOwner, this);
   for (const ActiveScrolledRoot* asr = aItem->GetActiveScrolledRoot();
-       asr;
+       asr && asr != aStopAtAsr;
        asr = asr->mParent) {
     Maybe<ScrollMetadata> metadata = asr->mScrollableFrame->ComputeScrollMetadata(
         nullptr, aItem->ReferenceFrame(), ContainerLayerParameters(), nullptr);
     MOZ_ASSERT(metadata);
     mScrollIds.AppendElement(aOwner.AddMetadata(metadata.ref()));
   }
 }
 
@@ -110,16 +97,23 @@ WebRenderLayerScrollData::GetDescendantC
 }
 
 size_t
 WebRenderLayerScrollData::GetScrollMetadataCount() const
 {
   return mScrollIds.Length();
 }
 
+void
+WebRenderLayerScrollData::AppendScrollMetadata(WebRenderScrollData& aOwner,
+                                               const ScrollMetadata& aData)
+{
+  mScrollIds.AppendElement(aOwner.AddMetadata(aData));
+}
+
 const ScrollMetadata&
 WebRenderLayerScrollData::GetScrollMetadata(const WebRenderScrollData& aOwner,
                                             size_t aIndex) const
 {
   MOZ_ASSERT(aIndex < mScrollIds.Length());
   return aOwner.GetScrollMetadata(mScrollIds[aIndex]);
 }
 
--- a/gfx/layers/wr/WebRenderScrollData.h
+++ b/gfx/layers/wr/WebRenderScrollData.h
@@ -15,16 +15,19 @@
 #include "mozilla/GfxMessageUtils.h"
 #include "mozilla/layers/LayerAttributes.h"
 #include "mozilla/layers/LayersMessageUtils.h"
 #include "mozilla/layers/FocusTarget.h"
 #include "mozilla/Maybe.h"
 #include "nsTArrayForwardDeclare.h"
 
 namespace mozilla {
+
+struct ActiveScrolledRoot;
+
 namespace layers {
 
 class Layer;
 class WebRenderScrollData;
 
 // Data needed by APZ, per layer. One instance of this class is created for
 // each layer in the layer tree and sent over PWebRenderBridge to the APZ code.
 // Each WebRenderLayerScrollData is conceptually associated with an "owning"
@@ -38,39 +41,51 @@ public:
   // Actually initialize the object. This is not done during the constructor
   // for optimization purposes (the call site is hard to write efficiently
   // if we do this in the constructor).
   void Initialize(WebRenderScrollData& aOwner,
                   Layer* aLayer,
                   int32_t aDescendantCount);
   void InitializeRoot(int32_t aDescendantCount);
   void Initialize(WebRenderScrollData& aOwner,
-                  nsDisplayItem* aItem);
+                  nsDisplayItem* aItem,
+                  int32_t aDescendantCount,
+                  const ActiveScrolledRoot* aStopAtAsr);
 
   int32_t GetDescendantCount() const;
   size_t GetScrollMetadataCount() const;
 
+  void AppendScrollMetadata(WebRenderScrollData& aOwner,
+                            const ScrollMetadata& aData);
   // Return the ScrollMetadata object that used to be on the original Layer
   // at the given index. Since we deduplicate the ScrollMetadata objects into
   // the array in the owning WebRenderScrollData object, we need to be passed
   // in a reference to that owner as well.
   const ScrollMetadata& GetScrollMetadata(const WebRenderScrollData& aOwner,
                                           size_t aIndex) const;
 
   gfx::Matrix4x4 GetTransform() const { return mTransform; }
   CSSTransformMatrix GetTransformTyped() const;
   bool GetTransformIsPerspective() const { return mTransformIsPerspective; }
   EventRegions GetEventRegions() const { return mEventRegions; }
   const LayerIntRegion& GetVisibleRegion() const { return mVisibleRegion; }
+  void SetReferentId(uint64_t aReferentId) { mReferentId = Some(aReferentId); }
   Maybe<uint64_t> GetReferentId() const { return mReferentId; }
   EventRegionsOverride GetEventRegionsOverride() const { return mEventRegionsOverride; }
+
+  void SetScrollThumbData(const ScrollThumbData& aData) { mScrollThumbData = aData; }
   const ScrollThumbData& GetScrollThumbData() const { return mScrollThumbData; }
+  void SetScrollbarAnimationId(const uint64_t& aId) { mScrollbarAnimationId = aId; }
   const uint64_t& GetScrollbarAnimationId() const { return mScrollbarAnimationId; }
+  void SetScrollbarTargetContainerId(FrameMetrics::ViewID aId) { mScrollbarTargetContainerId = aId; }
   FrameMetrics::ViewID GetScrollbarTargetContainerId() const { return mScrollbarTargetContainerId; }
+  void SetIsScrollbarContainer() { mIsScrollbarContainer = true; }
   bool IsScrollbarContainer() const { return mIsScrollbarContainer; }
+
+  void SetFixedPositionScrollContainerId(FrameMetrics::ViewID aId) { mFixedPosScrollContainerId = aId; }
   FrameMetrics::ViewID GetFixedPositionScrollContainerId() const { return mFixedPosScrollContainerId; }
 
   void Dump(const WebRenderScrollData& aOwner) const;
 
   friend struct IPC::ParamTraits<WebRenderLayerScrollData>;
 
 private:
   // The number of descendants this layer has (not including the layer itself).
--- a/gfx/thebes/gfxPrefs.h
+++ b/gfx/thebes/gfxPrefs.h
@@ -280,16 +280,17 @@ private:
   // a method accessing a pref already exists. Just add yours in the list.
 
   DECL_GFX_PREF(Live, "accessibility.browsewithcaret", AccessibilityBrowseWithCaret, bool, false);
 
   // The apz prefs are explained in AsyncPanZoomController.cpp
   DECL_GFX_PREF(Live, "apz.allow_checkerboarding",             APZAllowCheckerboarding, bool, true);
   DECL_GFX_PREF(Live, "apz.allow_immediate_handoff",           APZAllowImmediateHandoff, bool, true);
   DECL_GFX_PREF(Live, "apz.allow_zooming",                     APZAllowZooming, bool, false);
+  DECL_GFX_PREF(Live, "apz.autoscroll.enabled",                APZAutoscrollEnabled, bool, false);
   DECL_GFX_PREF(Live, "apz.axis_lock.breakout_angle",          APZAxisBreakoutAngle, float, float(M_PI / 8.0) /* 22.5 degrees */);
   DECL_GFX_PREF(Live, "apz.axis_lock.breakout_threshold",      APZAxisBreakoutThreshold, float, 1.0f / 32.0f);
   DECL_GFX_PREF(Live, "apz.axis_lock.direct_pan_angle",        APZAllowedDirectPanAngle, float, float(M_PI / 3.0) /* 60 degrees */);
   DECL_GFX_PREF(Live, "apz.axis_lock.lock_angle",              APZAxisLockAngle, float, float(M_PI / 6.0) /* 30 degrees */);
   DECL_GFX_PREF(Live, "apz.axis_lock.mode",                    APZAxisLockMode, int32_t, 0);
   DECL_GFX_PREF(Live, "apz.content_response_timeout",          APZContentResponseTimeout, int32_t, 400);
   DECL_GFX_PREF(Live, "apz.danger_zone_x",                     APZDangerZoneX, int32_t, 50);
   DECL_GFX_PREF(Live, "apz.danger_zone_y",                     APZDangerZoneY, int32_t, 100);
--- a/layout/base/GeckoRestyleManager.cpp
+++ b/layout/base/GeckoRestyleManager.cpp
@@ -136,17 +136,17 @@ GetNextBlockInInlineSibling(nsIFrame* aF
  * Since this is used when deciding to copy the new style context, it
  * takes as an argument the old style context to check if the style is
  * the same.  When it is used in other contexts (i.e., where the next
  * continuation would already have the new style context), the current
  * style context should be passed.
  */
 static nsIFrame*
 GetNextContinuationWithSameStyle(nsIFrame* aFrame,
-                                 nsStyleContext* aOldStyleContext,
+                                 GeckoStyleContext* aOldStyleContext,
                                  bool* aHaveMoreContinuations = nullptr)
 {
   // See GetPrevContinuationWithSameStyle about {ib} splits.
 
   nsIFrame* nextContinuation = aFrame->GetNextContinuation();
   if (!nextContinuation &&
       (aFrame->GetStateBits() & NS_FRAME_PART_OF_IBSPLIT)) {
     // We're the last continuation, so we have to hop back to the first
@@ -161,17 +161,17 @@ GetNextContinuationWithSameStyle(nsIFram
 
   if (!nextContinuation) {
     return nullptr;
   }
 
   NS_ASSERTION(nextContinuation->GetContent() == aFrame->GetContent(),
                "unexpected content mismatch");
 
-  nsStyleContext* nextStyle = nextContinuation->StyleContext();
+  GeckoStyleContext* nextStyle = nextContinuation->StyleContext()->AsGecko();
   if (nextStyle != aOldStyleContext) {
     NS_ASSERTION(aOldStyleContext->GetPseudo() != nextStyle->GetPseudo() ||
                  aOldStyleContext->GetParent() != nextStyle->GetParent(),
                  "continuations should have the same style context");
     nextContinuation = nullptr;
     if (aHaveMoreContinuations) {
       *aHaveMoreContinuations = true;
     }
@@ -917,18 +917,18 @@ GetPrevContinuationWithPossiblySameStyle
 static nsIFrame*
 GetPrevContinuationWithSameStyle(nsIFrame* aFrame)
 {
   nsIFrame* prevContinuation = GetPrevContinuationWithPossiblySameStyle(aFrame);
   if (!prevContinuation) {
     return nullptr;
   }
 
-  nsStyleContext* prevStyle = prevContinuation->StyleContext();
-  nsStyleContext* selfStyle = aFrame->StyleContext();
+  GeckoStyleContext* prevStyle = prevContinuation->StyleContext()->AsGecko();
+  GeckoStyleContext* selfStyle = aFrame->StyleContext()->AsGecko();
   if (prevStyle != selfStyle) {
     NS_ASSERTION(prevStyle->GetPseudo() != selfStyle->GetPseudo() ||
                  prevStyle->GetParent() != selfStyle->GetParent(),
                  "continuations should have the same style context");
     prevContinuation = nullptr;
   }
   return prevContinuation;
 }
@@ -976,18 +976,18 @@ GeckoRestyleManager::ReparentStyleContex
     // same style context is valid before the reresolution.  (We need
     // to check the pseudo-type and style context parent because of
     // :first-letter and :first-line, where we create styled and
     // unstyled letter/line frames distinguished by pseudo-type, and
     // then need to distinguish their descendants based on having
     // different parents.)
     nsIFrame* nextContinuation = aFrame->GetNextContinuation();
     if (nextContinuation) {
-      nsStyleContext* nextContinuationContext =
-        nextContinuation->StyleContext();
+      GeckoStyleContext* nextContinuationContext =
+        nextContinuation->StyleContext()->AsGecko();
       NS_ASSERTION(oldContext == nextContinuationContext ||
                    oldContext->GetPseudo() !=
                      nextContinuationContext->GetPseudo() ||
                    oldContext->GetParent() !=
                      nextContinuationContext->GetParent(),
                    "continuations should have the same style context");
     }
   }
@@ -1453,17 +1453,17 @@ ElementRestyler::ConditionallyRestyleCon
   MOZ_ASSERT(aFrame->GetContent()->IsElement());
   MOZ_ASSERT(!aFrame->GetContent()->IsStyledByServo());
 
   if (aFrame->GetContent()->HasFlag(mRestyleTracker.RootBit())) {
     aRestyleRoot = aFrame->GetContent()->AsElement();
   }
 
   for (nsIFrame* f = aFrame; f;
-       f = GetNextContinuationWithSameStyle(f, f->StyleContext())) {
+       f = GetNextContinuationWithSameStyle(f, f->StyleContext()->AsGecko())) {
     nsIFrame::ChildListIterator lists(f);
     for (; !lists.IsDone(); lists.Next()) {
       for (nsIFrame* child : lists.CurrentList()) {
         // Out-of-flows are reached through their placeholders.  Continuations
         // and block-in-inline splits are reached through those chains.
         if (!(child->GetStateBits() & NS_FRAME_OUT_OF_FLOW) &&
             !GetPrevContinuationWithSameStyle(child)) {
           // only do frames that are in flow
@@ -1733,17 +1733,17 @@ ElementRestyler::MoveStyleContextsForChi
   nsTArray<GeckoStyleContext*> contextsToMove;
 
   MOZ_ASSERT(!MustReframeForBeforePseudo(),
              "shouldn't need to reframe ::before as we would have had "
              "eRestyle_Subtree and wouldn't get in here");
 
   DebugOnly<nsIFrame*> lastContinuation;
   for (nsIFrame* f = mFrame; f;
-       f = GetNextContinuationWithSameStyle(f, f->StyleContext())) {
+       f = GetNextContinuationWithSameStyle(f, f->StyleContext()->AsGecko())) {
     lastContinuation = f;
     if (!MoveStyleContextsForContentChildren(f, aOldContext, contextsToMove)) {
       return false;
     }
   }
 
   MOZ_ASSERT(!MustReframeForAfterPseudo(lastContinuation),
              "shouldn't need to reframe ::after as we would have had "
@@ -2222,16 +2222,34 @@ ElementRestyler::ComputeRestyleResultFro
     aRestyleResult = RestyleResult::eContinue;
     // Continue to check other conditions if aCanStopWithStyleChange might
     // still need to be set to false.
     if (!aCanStopWithStyleChange) {
       return;
     }
   }
 
+  if (auto* position = oldContext->PeekStylePosition()) {
+    const bool wasLegacyJustifyItems =
+      position->mJustifyItems & NS_STYLE_JUSTIFY_LEGACY;
+    const auto newJustifyItems = aNewContext->StylePosition()->mJustifyItems;
+    const bool isLegacyJustifyItems =
+       newJustifyItems & NS_STYLE_JUSTIFY_LEGACY;
+
+    // Children with justify-items: legacy may depend on our value.
+    if (wasLegacyJustifyItems != isLegacyJustifyItems ||
+        (wasLegacyJustifyItems && position->mJustifyItems != newJustifyItems)) {
+      LOG_RESTYLE_CONTINUE("legacy justify-items changed between old and new"
+                           " style contexts");
+      aRestyleResult = RestyleResult::eContinue;
+      aCanStopWithStyleChange = false;
+      return;
+    }
+  }
+
   // If the old and new style contexts differ in their
   // NS_STYLE_HAS_TEXT_DECORATION_LINES or NS_STYLE_HAS_PSEUDO_ELEMENT_DATA
   // bits, then we must keep restyling so that those new bit values are
   // propagated.
   if (oldContext->HasTextDecorationLines() !=
         aNewContext->HasTextDecorationLines()) {
     LOG_RESTYLE_CONTINUE("NS_STYLE_HAS_TEXT_DECORATION_LINES differs between old"
                          " and new style contexts");
@@ -2996,17 +3014,17 @@ ElementRestyler::RestyleChildren(nsResty
   // kids would use mFrame->StyleContext(), which is out of date if
   // mHintsHandledBySelf has a ReconstructFrame hint; doing this could
   // trigger assertions about mismatched rule trees.
   nsIFrame* lastContinuation;
   if (!(mHintsHandledBySelf & nsChangeHint_ReconstructFrame)) {
     InitializeAccessibilityNotifications(mFrame->StyleContext());
 
     for (nsIFrame* f = mFrame; f;
-         f = GetNextContinuationWithSameStyle(f, f->StyleContext())) {
+         f = GetNextContinuationWithSameStyle(f, f->StyleContext()->AsGecko())) {
       lastContinuation = f;
       RestyleContentChildren(f, aChildRestyleHint);
     }
 
     SendAccessibilityNotifications();
   }
 
   // Check whether we might need to create a new ::after frame.
--- a/layout/base/RestyleManager.cpp
+++ b/layout/base/RestyleManager.cpp
@@ -528,21 +528,21 @@ DumpContext(nsIFrame* aFrame, nsStyleCon
       fputs(NS_LossyConvertUTF16toASCII(buffer).get(), stdout);
       fputs(" ", stdout);
     }
     fputs("{}\n", stdout);
   }
 }
 
 static void
-VerifySameTree(nsStyleContext* aContext1, nsStyleContext* aContext2)
+VerifySameTree(GeckoStyleContext* aContext1, GeckoStyleContext* aContext2)
 {
-  nsStyleContext* top1 = aContext1;
-  nsStyleContext* top2 = aContext2;
-  nsStyleContext* parent;
+  GeckoStyleContext* top1 = aContext1;
+  GeckoStyleContext* top2 = aContext2;
+  GeckoStyleContext* parent;
   for (;;) {
     parent = top1->GetParent();
     if (!parent)
       break;
     top1 = parent;
   }
   for (;;) {
     parent = top2->GetParent();
@@ -550,32 +550,33 @@ VerifySameTree(nsStyleContext* aContext1
       break;
     top2 = parent;
   }
   NS_ASSERTION(top1 == top2,
                "Style contexts are not in the same style context tree");
 }
 
 static void
-VerifyContextParent(nsIFrame* aFrame, nsStyleContext* aContext,
-                    nsStyleContext* aParentContext)
+VerifyContextParent(nsIFrame* aFrame, GeckoStyleContext* aContext,
+                    GeckoStyleContext* aParentContext)
 {
   // get the contexts not provided
   if (!aContext) {
-    aContext = aFrame->StyleContext();
+    aContext = aFrame->StyleContext()->AsGecko();
   }
 
   if (!aParentContext) {
     nsIFrame* providerFrame;
-    aParentContext = aFrame->GetParentStyleContext(&providerFrame);
+    nsStyleContext* parent = aFrame->GetParentStyleContext(&providerFrame);
+    aParentContext = parent ? parent->AsGecko() : nullptr;
     // aParentContext could still be null
   }
 
   NS_ASSERTION(aContext, "Failure to get required contexts");
-  nsStyleContext* actualParentContext = aContext->GetParent();
+  GeckoStyleContext* actualParentContext = aContext->GetParent();
 
   if (aParentContext) {
     if (aParentContext != actualParentContext) {
       DumpContext(aFrame, aContext);
       if (aContext == aParentContext) {
         NS_ERROR("Using parent's style context");
       } else {
         NS_ERROR("Wrong parent style context");
@@ -593,17 +594,17 @@ VerifyContextParent(nsIFrame* aFrame, ns
       NS_ERROR("Have parent context and shouldn't");
       DumpContext(aFrame, aContext);
       fputs("Has parent context: ", stdout);
       DumpContext(nullptr, actualParentContext);
       fputs("Should be null\n\n", stdout);
     }
   }
 
-  nsStyleContext* childStyleIfVisited = aContext->GetStyleIfVisited();
+  GeckoStyleContext* childStyleIfVisited = aContext->GetStyleIfVisited();
   // Either childStyleIfVisited has aContext->GetParent()->GetStyleIfVisited()
   // as the parent or it has a different rulenode from aContext _and_ has
   // aContext->GetParent() as the parent.
   if (childStyleIfVisited &&
       !((childStyleIfVisited->RuleNode() != aContext->RuleNode() &&
          childStyleIfVisited->GetParent() == aContext->GetParent()) ||
         childStyleIfVisited->GetParent() ==
           aContext->GetParent()->GetStyleIfVisited())) {
@@ -611,17 +612,17 @@ VerifyContextParent(nsIFrame* aFrame, ns
     DumpContext(aFrame, aContext);
     fputs("\n", stdout);
   }
 }
 
 static void
 VerifyStyleTree(nsIFrame* aFrame)
 {
-  nsStyleContext* context = aFrame->StyleContext();
+  GeckoStyleContext* context = aFrame->StyleContext()->AsGecko();
   VerifyContextParent(aFrame, context, nullptr);
 
   nsIFrame::ChildListIterator lists(aFrame);
   for (; !lists.IsDone(); lists.Next()) {
     for (nsIFrame* child : lists.CurrentList()) {
       if (!(child->GetStateBits() & NS_FRAME_OUT_OF_FLOW)) {
         // only do frames that are in flow
         if (child->IsPlaceholderFrame()) {
@@ -645,17 +646,17 @@ VerifyStyleTree(nsIFrame* aFrame)
     }
   }
 
   // do additional contexts
   int32_t contextIndex = 0;
   for (nsStyleContext* extraContext;
        (extraContext = aFrame->GetAdditionalStyleContext(contextIndex));
        ++contextIndex) {
-    VerifyContextParent(aFrame, extraContext, context);
+    VerifyContextParent(aFrame, extraContext->AsGecko(), context);
   }
 }
 
 void
 RestyleManager::DebugVerifyStyleTree(nsIFrame* aFrame)
 {
   if (IsServo()) {
     // XXXheycam For now, we know that we don't use the same inheritance
--- a/layout/base/nsCSSFrameConstructor.cpp
+++ b/layout/base/nsCSSFrameConstructor.cpp
@@ -5880,17 +5880,17 @@ nsCSSFrameConstructor::AddFrameConstruct
 
       if (newPendingBinding->mBinding) {
         pendingBinding = newPendingBinding;
         // aState takes over owning newPendingBinding
         aState.AddPendingBinding(newPendingBinding.forget());
       }
 
       if (resolveStyle) {
-        if (aContent->IsStyledByServo()) {
+        if (styleContext->IsServo()) {
           Element* element = aContent->AsElement();
           ServoStyleSet* styleSet = mPresShell->StyleSet()->AsServo();
 
           // XXX: We should have a better way to restyle ourselves.
           ServoRestyleManager::ClearServoDataFromSubtree(element);
           styleSet->StyleNewSubtree(element);
 
           // Servo's should_traverse_children() in traversal.rs skips
@@ -5900,17 +5900,18 @@ nsCSSFrameConstructor::AddFrameConstruct
 
           // Because of LazyComputeBehavior::Assert we never create a style
           // context here, so it's fine to pass a null parent.
           styleContext =
             styleSet->ResolveStyleFor(element, nullptr,
                                       LazyComputeBehavior::Assert);
         } else {
           styleContext =
-            ResolveStyleContext(styleContext->GetParent(), aContent, &aState);
+            ResolveStyleContext(styleContext->AsGecko()->GetParent(),
+                                aContent, &aState);
         }
 
         display = styleContext->StyleDisplay();
         aStyleContext = styleContext;
       }
 
       aTag = mDocument->BindingManager()->ResolveTag(aContent, &aNameSpaceID);
     } else if (display->mBinding.ForceGet()) {
@@ -9642,17 +9643,17 @@ nsCSSFrameConstructor::MaybeRecreateFram
     if (!oldContext) {
       return nullptr;
     }
     oldDisplay = StyleDisplay::Contents;
   }
 
   // The parent has a frame, so try resolving a new context.
   RefPtr<nsStyleContext> newContext = mPresShell->StyleSet()->
-    ResolveStyleFor(aElement, oldContext->GetParent(),
+    ResolveStyleFor(aElement, oldContext->AsGecko()->GetParent(),
                     LazyComputeBehavior::Assert);
 
   if (oldDisplay == StyleDisplay::None) {
     ChangeUndisplayedContent(aElement, newContext);
   } else {
     ChangeDisplayContents(aElement, newContext);
   }
 
@@ -11696,19 +11697,22 @@ static bool IsFirstLetterContent(nsICont
  * Create a letter frame, only make it a floating frame.
  */
 nsFirstLetterFrame*
 nsCSSFrameConstructor::CreateFloatingLetterFrame(
   nsFrameConstructorState& aState,
   nsIContent* aTextContent,
   nsIFrame* aTextFrame,
   nsContainerFrame* aParentFrame,
+  nsStyleContext* aParentStyleContext,
   nsStyleContext* aStyleContext,
   nsFrameItems& aResult)
 {
+  MOZ_ASSERT(aParentStyleContext);
+
   nsFirstLetterFrame* letterFrame =
     NS_NewFirstLetterFrame(mPresShell, aStyleContext);
   // We don't want to use a text content for a non-text frame (because we want
   // its primary frame to be a text frame).  So use its parent for the
   // first-letter.
   nsIContent* letterContent = aTextContent->GetParent();
   nsContainerFrame* containingBlock = aState.GetGeometricParent(
     aStyleContext->StyleDisplay(), aParentFrame);
@@ -11730,23 +11734,19 @@ nsCSSFrameConstructor::CreateFloatingLet
   // See if we will need to continue the text frame (does it contain
   // more than just the first-letter text or not?) If it does, then we
   // create (in advance) a continuation frame for it.
   nsIFrame* nextTextFrame = nullptr;
   if (NeedFirstLetterContinuation(aTextContent)) {
     // Create continuation
     nextTextFrame =
       CreateContinuingFrame(aState.mPresContext, aTextFrame, aParentFrame);
-    // Repair the continuations style context
-    nsStyleContext* parentStyleContext = aStyleContext->GetParentAllowServo();
-    if (parentStyleContext) {
-      RefPtr<nsStyleContext> newSC = styleSet->
-        ResolveStyleForText(aTextContent, parentStyleContext);
-      nextTextFrame->SetStyleContext(newSC);
-    }
+    RefPtr<nsStyleContext> newSC = styleSet->
+      ResolveStyleForText(aTextContent, aParentStyleContext);
+    nextTextFrame->SetStyleContext(newSC);
   }
 
   NS_ASSERTION(aResult.IsEmpty(), "aResult should be an empty nsFrameItems!");
   // Put the new float before any of the floats in the block we're doing
   // first-letter for, that is, before any floats whose parent is
   // containingBlock.
   nsFrameList::FrameLinkEnumerator link(aState.mFloatedItems);
   while (!link.AtEnd() && link.NextFrame()->GetParent() != containingBlock) {
@@ -11787,18 +11787,18 @@ nsCSSFrameConstructor::CreateLetterFrame
                                      nsCSSPseudoElements::firstLetter)->
       StyleContext();
 
   // Use content from containing block so that we can actually
   // find a matching style rule.
   nsIContent* blockContent = aBlockFrame->GetContent();
 
   // Create first-letter style rule
-  RefPtr<nsStyleContext> sc = GetFirstLetterStyle(blockContent,
-                                                    parentStyleContext);
+  RefPtr<nsStyleContext> sc =
+    GetFirstLetterStyle(blockContent, parentStyleContext);
   if (sc) {
     RefPtr<nsStyleContext> textSC = mPresShell->StyleSet()->
       ResolveStyleForText(aTextContent, sc);
 
     // Create a new text frame (the original one will be discarded)
     // pass a temporary stylecontext, the correct one will be set
     // later.  Start off by unsetting the primary frame for
     // aTextContent, so it's no longer pointing to the to-be-destroyed
@@ -11819,17 +11819,18 @@ nsCSSFrameConstructor::CreateLetterFrame
 
     // Create the right type of first-letter frame
     const nsStyleDisplay* display = sc->StyleDisplay();
     nsFirstLetterFrame* letterFrame;
     if (display->IsFloatingStyle() &&
         !nsSVGUtils::IsInSVGTextSubtree(aParentFrame)) {
       // Make a floating first-letter frame
       letterFrame = CreateFloatingLetterFrame(state, aTextContent, textFrame,
-                                              aParentFrame, sc, aResult);
+                                              aParentFrame, parentStyleContext,
+                                              sc, aResult);
     }
     else {
       // Make an inflow first-letter frame
       letterFrame = NS_NewFirstLetterFrame(mPresShell, sc);
 
       // Initialize the first-letter-frame.  We don't want to use a text
       // content for a non-text frame (because we want its primary frame to
       // be a text frame).  So use its parent for the first-letter.
--- a/layout/base/nsCSSFrameConstructor.h
+++ b/layout/base/nsCSSFrameConstructor.h
@@ -1944,16 +1944,17 @@ private:
 
   // Methods support :first-letter style
 
   nsFirstLetterFrame*
   CreateFloatingLetterFrame(nsFrameConstructorState& aState,
                             nsIContent*              aTextContent,
                             nsIFrame*                aTextFrame,
                             nsContainerFrame*        aParentFrame,
+                            nsStyleContext*          aParentStyleContext,
                             nsStyleContext*          aStyleContext,
                             nsFrameItems&            aResult);
 
   void CreateLetterFrame(nsContainerFrame*        aBlockFrame,
                          nsContainerFrame*        aBlockContinuation,
                          nsIContent*              aTextContent,
                          nsContainerFrame*        aParentFrame,
                          nsFrameItems&            aResult);
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -2082,23 +2082,30 @@ nsLayoutUtils::SetFixedPositionLayerData
       } else {
         anchor.y = anchorRect.YMost();
       }
     } else if (position->mOffset.GetTopUnit() != eStyleUnit_Auto) {
       sides |= eSideBitsTop;
     }
   }
 
+  ViewID id = ScrollIdForRootScrollFrame(aPresContext);
+  aLayer->SetFixedPositionData(id, anchor, sides);
+}
+
+FrameMetrics::ViewID
+nsLayoutUtils::ScrollIdForRootScrollFrame(nsPresContext* aPresContext)
+{
   ViewID id = FrameMetrics::NULL_SCROLL_ID;
   if (nsIFrame* rootScrollFrame = aPresContext->PresShell()->GetRootScrollFrame()) {
     if (nsIContent* content = rootScrollFrame->GetContent()) {
       id = FindOrCreateIDFor(content);
     }
   }
-  aLayer->SetFixedPositionData(id, anchor, sides);
+  return id;
 }
 
 bool
 nsLayoutUtils::ViewportHasDisplayPort(nsPresContext* aPresContext)
 {
   nsIFrame* rootScrollFrame =
     aPresContext->PresShell()->GetRootScrollFrame();
   return rootScrollFrame &&
--- a/layout/base/nsLayoutUtils.h
+++ b/layout/base/nsLayoutUtils.h
@@ -568,16 +568,22 @@ public:
    */
   static void SetFixedPositionLayerData(Layer* aLayer, const nsIFrame* aViewportFrame,
                                         const nsRect& aAnchorRect,
                                         const nsIFrame* aFixedPosFrame,
                                         nsPresContext* aPresContext,
                                         const ContainerLayerParameters& aContainerParameters);
 
   /**
+   * Get the scroll id for the root scrollframe of the presshell of the given
+   * prescontext. Returns NULL_SCROLL_ID if it couldn't be found.
+   */
+  static FrameMetrics::ViewID ScrollIdForRootScrollFrame(nsPresContext* aPresContext);
+
+  /**
    * Return true if aPresContext's viewport has a displayport.
    */
   static bool ViewportHasDisplayPort(nsPresContext* aPresContext);
 
   /**
    * Return true if aFrame is a fixed-pos frame and is a child of a viewport
    * which has a displayport. These frames get special treatment from the compositor.
    * aDisplayPort, if non-null, is set to the display port rectangle (relative to
--- a/layout/base/nsPresContext.cpp
+++ b/layout/base/nsPresContext.cpp
@@ -2257,19 +2257,19 @@ nsPresContext::UpdateIsChrome()
   mIsChrome = mContainer &&
               nsIDocShellTreeItem::typeChrome == mContainer->ItemType();
 }
 
 bool
 nsPresContext::HasAuthorSpecifiedRules(const nsIFrame* aFrame,
                                        uint32_t aRuleTypeMask) const
 {
-  if (mShell->StyleSet()->IsGecko()) {
+  if (auto* geckoStyleContext = aFrame->StyleContext()->GetAsGecko()) {
     return
-      nsRuleNode::HasAuthorSpecifiedRules(aFrame->StyleContext(),
+      nsRuleNode::HasAuthorSpecifiedRules(geckoStyleContext,
                                           aRuleTypeMask,
                                           UseDocumentColors());
   }
   Element* elem = aFrame->GetContent()->AsElement();
 
   MOZ_ASSERT(elem->GetPseudoElementType() ==
              aFrame->StyleContext()->GetPseudoType());
   MOZ_ASSERT(elem->HasServoData());
--- a/layout/generic/nsBlockFrame.cpp
+++ b/layout/generic/nsBlockFrame.cpp
@@ -1526,23 +1526,21 @@ nsBlockFrame::Reflow(nsPresContext*     
               (value == NS_STYLE_ALIGN_AUTO));
     };
 
     // First check this frame for non-default values of the css-align properties
     // that apply to block containers.
     // Note: we check here for non-default "justify-items", though technically
     // that'd only affect rendering if some child has "justify-self:auto".
     // (It's safe to assume that's likely, since it's the default value that
-    // a child would have.) We also pass in nullptr for the parent style context
-    // because an accurate parameter is slower and only necessary to detect a
-    // narrow edge case with the "legacy" keyword.
+    // a child would have.)
     const nsStylePosition* stylePosition = reflowInput->mStylePosition;
     if (!IsStyleNormalOrAuto(stylePosition->mJustifyContent) ||
         !IsStyleNormalOrAuto(stylePosition->mAlignContent) ||
-        !IsStyleNormalOrAuto(stylePosition->ComputedJustifyItems(nullptr))) {
+        !IsStyleNormalOrAuto(stylePosition->mJustifyItems)) {
       Telemetry::Accumulate(Telemetry::BOX_ALIGN_PROPS_IN_BLOCKS_FLAG, true);
     } else {
       // If not already flagged by the parent, now check justify-self of the
       // block-level child frames.
       for (nsBlockFrame::LineIterator line = LinesBegin();
            line != LinesEnd(); ++line) {
         if (line->IsBlock() &&
             !IsStyleNormalOrAuto(line->mFirstChild->StylePosition()->mJustifySelf)) {
--- a/layout/generic/nsFirstLetterFrame.cpp
+++ b/layout/generic/nsFirstLetterFrame.cpp
@@ -54,23 +54,23 @@ void
 nsFirstLetterFrame::Init(nsIContent*       aContent,
                          nsContainerFrame* aParent,
                          nsIFrame*         aPrevInFlow)
 {
   RefPtr<nsStyleContext> newSC;
   if (aPrevInFlow) {
     // Get proper style context for ourselves.  We're creating the frame
     // that represents everything *except* the first letter, so just create
-    // a style context like we would for a text node.
-    nsStyleContext* parentStyleContext = mStyleContext->GetParentAllowServo();
-    if (parentStyleContext) {
-      newSC = PresContext()->StyleSet()->
-        ResolveStyleForFirstLetterContinuation(parentStyleContext);
-      SetStyleContextWithoutNotification(newSC);
-    }
+    // a style context that inherits from our style parent, with no extra rules.
+    nsIFrame* styleParent =
+      CorrectStyleParentFrame(aParent, nsCSSPseudoElements::firstLetter);
+    nsStyleContext* parentStyleContext = styleParent->StyleContext();
+    newSC = PresContext()->StyleSet()->
+      ResolveStyleForFirstLetterContinuation(parentStyleContext);
+    SetStyleContextWithoutNotification(newSC);
   }
 
   nsContainerFrame::Init(aContent, aParent, aPrevInFlow);
 }
 
 void
 nsFirstLetterFrame::SetInitialChildList(ChildListID  aListID,
                                         nsFrameList& aChildList)
@@ -373,24 +373,33 @@ nsFirstLetterFrame::DrainOverflowFrames(
     mFrames.AppendFrames(nullptr, *overflowFrames);
   }
 
   // Now repair our first frames style context (since we only reflow
   // one frame there is no point in doing any other ones until they
   // are reflowed)
   nsIFrame* kid = mFrames.FirstChild();
   if (kid) {
-    RefPtr<nsStyleContext> sc;
     nsIContent* kidContent = kid->GetContent();
     if (kidContent) {
       NS_ASSERTION(kidContent->IsNodeOfType(nsINode::eTEXT),
                    "should contain only text nodes");
-      nsStyleContext* parentSC = prevInFlow ? mStyleContext->GetParentAllowServo() :
-                                              mStyleContext;
-      sc = aPresContext->StyleSet()->ResolveStyleForText(kidContent, parentSC);
+      nsStyleContext* parentSC;
+      if (prevInFlow) {
+        // This is for the rest of the content not in the first-letter.
+        nsIFrame* styleParent =
+          CorrectStyleParentFrame(GetParent(),
+                                  nsCSSPseudoElements::firstLetter);
+        parentSC = styleParent->StyleContext();
+      } else {
+        // And this for the first-letter style.
+        parentSC = mStyleContext;
+      }
+      RefPtr<nsStyleContext> sc =
+        aPresContext->StyleSet()->ResolveStyleForText(kidContent, parentSC);
       kid->SetStyleContext(sc);
       nsLayoutUtils::MarkDescendantsDirty(kid);
     }
   }
 }
 
 nscoord
 nsFirstLetterFrame::GetLogicalBaseline(WritingMode aWritingMode) const
--- a/layout/generic/nsFrame.cpp
+++ b/layout/generic/nsFrame.cpp
@@ -7196,24 +7196,24 @@ nsIFrame::ListGeneric(nsACString& aTo, c
   aTo += nsPrintfCString(" [sc=%p", static_cast<void*>(mStyleContext));
   if (mStyleContext) {
     nsIAtom* pseudoTag = mStyleContext->GetPseudo();
     if (pseudoTag) {
       nsAutoString atomString;
       pseudoTag->ToString(atomString);
       aTo += nsPrintfCString("%s", NS_LossyConvertUTF16toASCII(atomString).get());
     }
-    if (mStyleContext->IsGecko()) {
-      if (!mStyleContext->GetParent() ||
-          (GetParent() && GetParent()->StyleContext() != mStyleContext->GetParent())) {
-        aTo += nsPrintfCString("^%p", mStyleContext->GetParent());
-        if (mStyleContext->GetParent()) {
-          aTo += nsPrintfCString("^%p", mStyleContext->GetParent()->GetParent());
-          if (mStyleContext->GetParent()->GetParent()) {
-            aTo += nsPrintfCString("^%p", mStyleContext->GetParent()->GetParent()->GetParent());
+    if (auto* geckoContext = mStyleContext->GetAsGecko()) {
+      if (!geckoContext->GetParent() ||
+          (GetParent() && GetParent()->StyleContext() != geckoContext->GetParent())) {
+        aTo += nsPrintfCString("^%p", geckoContext->GetParent());
+        if (geckoContext->GetParent()) {
+          aTo += nsPrintfCString("^%p", geckoContext->GetParent()->GetParent());
+          if (geckoContext->GetParent()->GetParent()) {
+            aTo += nsPrintfCString("^%p", geckoContext->GetParent()->GetParent()->GetParent());
           }
         }
       }
     }
   }
   aTo += "]";
 }
 
--- a/layout/generic/nsTextFrame.cpp
+++ b/layout/generic/nsTextFrame.cpp
@@ -4091,31 +4091,40 @@ nsTextPaintStyle::InitSelectionColorsAnd
   mInitSelectionColorsAndShadow = true;
 
   nsIFrame* nonGeneratedAncestor = nsLayoutUtils::GetNonGeneratedAncestor(mFrame);
   Element* selectionElement =
     FindElementAncestorForMozSelection(nonGeneratedAncestor->GetContent());
 
   if (selectionElement &&
       selectionStatus == nsISelectionController::SELECTION_ON) {
-    RefPtr<nsStyleContext> sc = nullptr;
-    sc = mPresContext->StyleSet()->
-      ProbePseudoElementStyle(selectionElement,
-                              CSSPseudoElementType::mozSelection,
-                              mFrame->StyleContext());
+    RefPtr<nsStyleContext> sc =
+      mPresContext->StyleSet()->
+        ProbePseudoElementStyle(selectionElement,
+                                CSSPseudoElementType::mozSelection,
+                                mFrame->StyleContext());
     // Use -moz-selection pseudo class.
     if (sc) {
       mSelectionBGColor =
         sc->GetVisitedDependentColor(&nsStyleBackground::mBackgroundColor);
       mSelectionTextColor =
         sc->GetVisitedDependentColor(&nsStyleText::mWebkitTextFillColor);
-      mHasSelectionShadow =
-        nsRuleNode::HasAuthorSpecifiedRules(sc,
-                                            NS_AUTHOR_SPECIFIED_TEXT_SHADOW,
-                                            true);
+      if (auto* geckoStyleContext = sc->GetAsGecko()) {
+        mHasSelectionShadow =
+          nsRuleNode::HasAuthorSpecifiedRules(geckoStyleContext,
+                                              NS_AUTHOR_SPECIFIED_TEXT_SHADOW,
+                                              true);
+      } else {
+        NS_WARNING("stylo: Need a way to get HasAuthorSpecifiedRules from a "
+                   "raw style context");
+        // Or at least an element and a pseudo-style, which is probably a bit
+        // more doable, since we know that, at least when not in the presence of
+        // first-line / first-letter, we're inheriting from selectionElement.
+        mHasSelectionShadow = true;
+      }
       if (mHasSelectionShadow) {
         mSelectionShadow = sc->StyleText()->mTextShadow;
       }
       return true;
     }
   }
 
   nscolor selectionBGColor =
--- a/layout/ipc/RenderFrameParent.cpp
+++ b/layout/ipc/RenderFrameParent.cpp
@@ -27,16 +27,17 @@
 #include "nsStyleStructInlines.h"
 #include "nsSubDocumentFrame.h"
 #include "nsView.h"
 #include "RenderFrameParent.h"
 #include "mozilla/gfx/GPUProcessManager.h"
 #include "mozilla/layers/LayerManagerComposite.h"
 #include "mozilla/layers/CompositorBridgeChild.h"
 #include "mozilla/layers/WebRenderLayerManager.h"
+#include "mozilla/layers/WebRenderScrollData.h"
 #include "mozilla/webrender/WebRenderAPI.h"
 #include "ClientLayerManager.h"
 #include "FrameLayerBuilder.h"
 
 using namespace mozilla::dom;
 using namespace mozilla::gfx;
 using namespace mozilla::layers;
 
@@ -395,13 +396,23 @@ nsDisplayRemote::CreateWebRenderCommands
   visible += mozilla::layout::GetContentRectLayerOffset(mFrame, aDisplayListBuilder);
 
   aBuilder.PushIFrame(aSc.ToRelativeLayoutRect(visible),
       mozilla::wr::AsPipelineId(GetRemoteLayersId()));
 
   return true;
 }
 
+bool
+nsDisplayRemote::UpdateScrollData(mozilla::layers::WebRenderScrollData* aData,
+                                  mozilla::layers::WebRenderLayerScrollData* aLayerData)
+{
+  if (aLayerData) {
+    aLayerData->SetReferentId(GetRemoteLayersId());
+  }
+  return true;
+}
+
 uint64_t
 nsDisplayRemote::GetRemoteLayersId() const
 {
   return mRemoteFrame->GetLayersId();
 }
--- a/layout/ipc/RenderFrameParent.h
+++ b/layout/ipc/RenderFrameParent.h
@@ -162,16 +162,19 @@ public:
   BuildLayer(nsDisplayListBuilder* aBuilder, LayerManager* aManager,
              const ContainerLayerParameters& aContainerParameters) override;
 
   virtual bool CreateWebRenderCommands(mozilla::wr::DisplayListBuilder& aBuilder,
                                        const StackingContextHelper& aSc,
                                        nsTArray<WebRenderParentCommand>& aParentCommands,
                                        mozilla::layers::WebRenderLayerManager* aManager,
                                        nsDisplayListBuilder* aDisplayListBuilder) override;
+  virtual bool UpdateScrollData(mozilla::layers::WebRenderScrollData* aData,
+                                mozilla::layers::WebRenderLayerScrollData* aLayerData) override;
+
   uint64_t GetRemoteLayersId() const;
 
   NS_DISPLAY_DECL_NAME("Remote", TYPE_REMOTE)
 
 private:
   RenderFrameParent* mRemoteFrame;
   mozilla::layers::EventRegionsOverride mEventRegionsOverride;
 };
--- a/layout/painting/nsDisplayList.cpp
+++ b/layout/painting/nsDisplayList.cpp
@@ -83,20 +83,20 @@
 #include "nsCSSProps.h"
 #include "nsPluginFrame.h"
 #include "nsSVGMaskFrame.h"
 #include "nsTableCellFrame.h"
 #include "nsTableColFrame.h"
 #include "ClientLayerManager.h"
 #include "mozilla/layers/StackingContextHelper.h"
 #include "mozilla/layers/WebRenderBridgeChild.h"
+#include "mozilla/layers/WebRenderDisplayItemLayer.h"
 #include "mozilla/layers/WebRenderLayerManager.h"
-#include "mozilla/layers/WebRenderDisplayItemLayer.h"
 #include "mozilla/layers/WebRenderMessages.h"
-#include "mozilla/layers/WebRenderDisplayItemLayer.h"
+#include "mozilla/layers/WebRenderScrollData.h"
 
 // GetCurrentTime is defined in winbase.h as zero argument macro forwarding to
 // GetTickCount().
 #ifdef GetCurrentTime
 #undef GetCurrentTime
 #endif
 
 using namespace mozilla;
@@ -6385,16 +6385,38 @@ nsDisplayOwnLayer::BuildLayer(nsDisplayL
   }
 
   if (mFlags & GENERATE_SUBDOC_INVALIDATIONS) {
     mFrame->PresContext()->SetNotifySubDocInvalidationData(layer);
   }
   return layer.forget();
 }
 
+bool
+nsDisplayOwnLayer::UpdateScrollData(mozilla::layers::WebRenderScrollData* aData,
+                                    mozilla::layers::WebRenderLayerScrollData* aLayerData)
+{
+  bool ret = false;
+  if (IsScrollThumbLayer()) {
+    ret = true;
+    if (aLayerData) {
+      aLayerData->SetScrollThumbData(mThumbData);
+      aLayerData->SetScrollbarTargetContainerId(mScrollTarget);
+    }
+  }
+  if (mFlags & SCROLLBAR_CONTAINER) {
+    ret = true;
+    if (aLayerData) {
+      aLayerData->SetIsScrollbarContainer();
+      aLayerData->SetScrollbarTargetContainerId(mScrollTarget);
+    }
+  }
+  return ret;
+}
+
 nsDisplaySubDocument::nsDisplaySubDocument(nsDisplayListBuilder* aBuilder,
                                            nsIFrame* aFrame, nsDisplayList* aList,
                                            uint32_t aFlags)
     : nsDisplayOwnLayer(aBuilder, aFrame, aList, aBuilder->CurrentActiveScrolledRoot(), aFlags)
     , mScrollParentId(aBuilder->GetCurrentScrollParentId())
 {
   MOZ_COUNT_CTOR(nsDisplaySubDocument);
   mForceDispatchToContentRegion =
@@ -6683,16 +6705,28 @@ bool nsDisplayFixedPosition::TryMerge(ns
   if (other->mFrame != mFrame)
     return false;
   if (aItem->GetClipChain() != GetClipChain())
     return false;
   MergeFromTrackingMergedFrames(other);
   return true;
 }
 
+bool
+nsDisplayFixedPosition::UpdateScrollData(mozilla::layers::WebRenderScrollData* aData,
+                                         mozilla::layers::WebRenderLayerScrollData* aLayerData)
+{
+  if (aLayerData) {
+    FrameMetrics::ViewID id = nsLayoutUtils::ScrollIdForRootScrollFrame(
+        Frame()->PresContext());
+    aLayerData->SetFixedPositionScrollContainerId(id);
+  }
+  return nsDisplayOwnLayer::UpdateScrollData(aData, aLayerData) | true;
+}
+
 TableType
 GetTableTypeFromFrame(nsIFrame* aFrame)
 {
   if (aFrame->IsTableFrame()) {
     return TableType::TABLE;
   }
 
   if (aFrame->IsTableColFrame()) {
@@ -6892,17 +6926,29 @@ nsDisplayScrollInfoLayer::ComputeScrollM
       mScrolledFrame, mScrollFrame, mScrollFrame->GetContent(),
       ReferenceFrame(), aLayer,
       mScrollParentId, viewport, Nothing(), false, aContainerParameters);
   metadata.GetMetrics().SetIsScrollInfoLayer(true);
 
   return UniquePtr<ScrollMetadata>(new ScrollMetadata(metadata));
 }
 
-
+bool
+nsDisplayScrollInfoLayer::UpdateScrollData(mozilla::layers::WebRenderScrollData* aData,
+                                           mozilla::layers::WebRenderLayerScrollData* aLayerData)
+{
+  if (aLayerData) {
+    UniquePtr<ScrollMetadata> metadata =
+      ComputeScrollMetadata(nullptr, ContainerLayerParameters());
+    MOZ_ASSERT(aData);
+    MOZ_ASSERT(metadata);
+    aLayerData->AppendScrollMetadata(*aData, *metadata);
+  }
+  return true;
+}
 
 void
 nsDisplayScrollInfoLayer::WriteDebugInfo(std::stringstream& aStream)
 {
   aStream << " (scrollframe " << mScrollFrame
           << " scrolledFrame " << mScrolledFrame << ")";
 }
 
--- a/layout/painting/nsDisplayList.h
+++ b/layout/painting/nsDisplayList.h
@@ -59,16 +59,18 @@ class FrameLayerBuilder;
 namespace layers {
 class Layer;
 class ImageLayer;
 class ImageContainer;
 class StackingContextHelper;
 class WebRenderCommand;
 class WebRenderParentCommand;
 class WebRenderDisplayItemLayer;
+class WebRenderScrollData;
+class WebRenderLayerScrollData;
 } // namespace layers
 namespace wr {
 class DisplayListBuilder;
 } // namespace wr
 } // namespace mozilla
 
 // A set of blend modes, that never includes OP_OVER (since it's
 // considered the default, rather than a specific blend mode).
@@ -1946,38 +1948,57 @@ public:
    * constructed.
    */
   virtual already_AddRefed<Layer> BuildLayer(nsDisplayListBuilder* aBuilder,
                                              LayerManager* aManager,
                                              const ContainerLayerParameters& aContainerParameters)
   { return nullptr; }
 
   /**
-    * Function to create the WebRenderCommands without
-    * Layer. For layers mode, aManager->IsLayersFreeTransaction()
-    * should be false to prevent doing GetLayerState again. For
-    * layers-free mode, we should check if the layer state is
-    * active first and have an early return if the layer state is
-    * not active.
-    *
-    * @return true if successfully creating webrender commands.
-    */
-   virtual bool CreateWebRenderCommands(mozilla::wr::DisplayListBuilder& aBuilder,
-                                        const StackingContextHelper& aSc,
-                                        nsTArray<WebRenderParentCommand>& aParentCommands,
-                                        mozilla::layers::WebRenderLayerManager* aManager,
-                                        nsDisplayListBuilder* aDisplayListBuilder) { return false; }
+   * Function to create the WebRenderCommands without
+   * Layer. For layers mode, aManager->IsLayersFreeTransaction()
+   * should be false to prevent doing GetLayerState again. For
+   * layers-free mode, we should check if the layer state is
+   * active first and have an early return if the layer state is
+   * not active.
+   *
+   * @return true if successfully creating webrender commands.
+   */
+  virtual bool CreateWebRenderCommands(mozilla::wr::DisplayListBuilder& aBuilder,
+                                       const StackingContextHelper& aSc,
+                                       nsTArray<WebRenderParentCommand>& aParentCommands,
+                                       mozilla::layers::WebRenderLayerManager* aManager,
+                                       nsDisplayListBuilder* aDisplayListBuilder) { return false; }
+
+  /**
+   * Updates the provided aLayerData with any APZ-relevant scroll data
+   * that is specific to this display item. This is stuff that would normally
+   * be put on the layer during BuildLayer, but this is only called in
+   * layers-free webrender mode, where we don't have lay