Merge mozilla-central to inbound. a=merge CLOSED TREE
authorshindli <shindli@mozilla.com>
Fri, 27 Apr 2018 00:45:48 +0300
changeset 472064 f679e7f1a758cd0f36adb333ebe0e5dc25f07b78
parent 472063 442c41eb3ab1be47e960e344305b9421ac944f75 (current diff)
parent 471971 63a0e2f626febb98d87d2543955ab99a653654ff (diff)
child 472065 4e267d999797b9631d4e3b5b89776f4c37d1b3c8
push id1728
push userjlund@mozilla.com
push dateMon, 18 Jun 2018 21:12:27 +0000
treeherdermozilla-release@c296fde26f5f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone61.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 mozilla-central to inbound. a=merge CLOSED TREE
testing/web-platform/meta/webdriver/tests/element_click/select.py.ini
--- a/.hgtags
+++ b/.hgtags
@@ -136,8 +136,9 @@ f9605772a0c9098ed1bcaa98089b2c944ed69e9b
 320642944e42a889db13c6c55b404e32319d4de6 FIREFOX_BETA_56_BASE
 8e818b5e9b6bef0fc1a5c527ecf30b0d56a02f14 FIREFOX_BETA_57_BASE
 f7e9777221a34f9f23c2e4933307eb38b621b679 FIREFOX_NIGHTLY_57_END
 40a14ca1cf04499f398e4cb8ba359b39eae4e216 FIREFOX_BETA_58_BASE
 1f91961bb79ad06fd4caef9e5dfd546afd5bf42c FIREFOX_NIGHTLY_58_END
 5faab9e619901b1513fd4ca137747231be550def FIREFOX_NIGHTLY_59_END
 e33efdb3e1517d521deb949de3fcd6d9946ea440 FIREFOX_BETA_60_BASE
 fdd1a0082c71673239fc2f3a6a93de889c07a1be FIREFOX_NIGHTLY_60_END
+ccfd7b716a91241ddbc084cb7116ec561e56d5d1 FIREFOX_BETA_61_BASE
--- a/browser/base/content/test/performance/browser_window_resize.js
+++ b/browser/base/content/test/performance/browser_window_resize.js
@@ -8,37 +8,19 @@
  * 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 = [
-  {
-    stack: [
-      "onOverflow@resource:///modules/CustomizableUI.jsm",
-    ],
-    maxCount: 48,
-  },
-
-  {
-    stack: [
-      "_moveItemsBackToTheirOrigin@resource:///modules/CustomizableUI.jsm",
-      "_onLazyResize@resource:///modules/CustomizableUI.jsm",
-    ],
-    maxCount: 5,
-  },
-
-  {
-    stack: [
-      "_onLazyResize@resource:///modules/CustomizableUI.jsm",
-    ],
-    maxCount: 4,
-  },
+   /**
+   * Nothing here! Please don't add anything new!
+   */
 ];
 
 const gToolbar = document.getElementById("PersonalToolbar");
 
 /**
  * Sets the visibility state on the Bookmarks Toolbar, and
  * waits for it to transition to fully visible.
  *
--- a/browser/components/customizableui/CustomizableUI.jsm
+++ b/browser/components/customizableui/CustomizableUI.jsm
@@ -4403,28 +4403,46 @@ OverflowableToolbar.prototype = {
     doc.defaultView.updateEditUIVisibility();
     let contextMenuId = this._panel.getAttribute("context");
     if (contextMenuId) {
       let contextMenu = doc.getElementById(contextMenuId);
       gELS.removeSystemEventListener(contextMenu, "command", this, true);
     }
   },
 
-  onOverflow(aEvent) {
-    // The rangeParent check is here because of bug 1111986 and ensuring that
-    // overflow events from the bookmarks toolbar items or similar things that
-    // manage their own overflow don't trigger an overflow on the entire toolbar
-    if (!this._enabled ||
-        (aEvent && aEvent.target != this._toolbar.customizationTarget) ||
-        (aEvent && aEvent.rangeParent))
+  /**
+   * Avoid re-entrancy in the overflow handling by keeping track of invocations:
+   */
+  _lastOverflowCounter: 0,
+
+  /**
+   * Handle overflow in the toolbar by moving items to the overflow menu.
+   * @param {Event} aEvent
+   *        The overflow event that triggered handling overflow. May be omitted
+   *        in some cases (e.g. when we run this method after overflow handling
+   *        is re-enabled from customize mode, to ensure correct handling of
+   *        initial overflow).
+   */
+  async onOverflow(aEvent) {
+    if (!this._enabled)
       return;
 
     let child = this._target.lastChild;
 
-    while (child && this._target.scrollLeftMin != this._target.scrollLeftMax) {
+    let thisOverflowResponse = ++this._lastOverflowCounter;
+
+    let win = this._target.ownerGlobal;
+    let [scrollLeftMin, scrollLeftMax] = await win.promiseDocumentFlushed(() => {
+      return [this._target.scrollLeftMin, this._target.scrollLeftMax];
+    });
+    if (win.closed || this._lastOverflowCounter != thisOverflowResponse) {
+      return;
+    }
+
+    while (child && scrollLeftMin != scrollLeftMax) {
       let prevChild = child.previousSibling;
 
       if (child.getAttribute("overflows") != "false") {
         this._collapsed.set(child.id, this._target.clientWidth);
         child.setAttribute("overflowedItem", true);
         child.setAttribute("cui-anchorid", this._chevron.id);
         CustomizableUIInternal.ensureButtonContextMenu(child, this._toolbar, true);
         CustomizableUIInternal.notifyListeners("onWidgetOverflow", child, this._target);
@@ -4433,40 +4451,70 @@ OverflowableToolbar.prototype = {
         if (!this._addedListener) {
           CustomizableUI.addListener(this);
         }
         if (!CustomizableUI.isSpecialWidget(child.id)) {
           this._toolbar.setAttribute("overflowing", "true");
         }
       }
       child = prevChild;
-    }
-
-    let win = this._target.ownerGlobal;
+      [scrollLeftMin, scrollLeftMax] = await win.promiseDocumentFlushed(() => {
+        return [this._target.scrollLeftMin, this._target.scrollLeftMax];
+      });
+      // If the window has closed or if we re-enter because we were waiting
+      // for layout, stop.
+      if (win.closed || this._lastOverflowCounter != thisOverflowResponse) {
+        return;
+      }
+    }
+
     win.UpdateUrlbarSearchSplitterState();
+    // Reset the counter because we finished handling overflow.
+    this._lastOverflowCounter = 0;
   },
 
   _onResize(aEvent) {
+    // Ignore bubbled-up resize events.
+    if (aEvent.target != aEvent.target.ownerGlobal.top) {
+      return;
+    }
     if (!this._lazyResizeHandler) {
       this._lazyResizeHandler = new DeferredTask(this._onLazyResize.bind(this),
                                                  LAZY_RESIZE_INTERVAL_MS, 0);
     }
     this._lazyResizeHandler.arm();
   },
 
-  _moveItemsBackToTheirOrigin(shouldMoveAllItems) {
+  /**
+   * Try to move toolbar items back to the toolbar from the overflow menu.
+   * @param {boolean} shouldMoveAllItems
+   *        Whether we should move everything (e.g. because we're being disabled)
+   * @param {number} targetWidth
+   *        Optional; the width of the toolbar in which we can put things.
+   *        Some consumers pass this to avoid reflows.
+   *        While there are items in the list, this width won't change, and so
+   *        we can avoid flushing layout by providing it and/or caching it.
+   *        Note that if `shouldMoveAllItems` is true, we never need the width
+   *        anyway.
+   */
+  _moveItemsBackToTheirOrigin(shouldMoveAllItems, targetWidth) {
     let placements = gPlacements.get(this._toolbar.id);
+    let win = this._target.ownerGlobal;
     while (this._list.firstChild) {
       let child = this._list.firstChild;
       let minSize = this._collapsed.get(child.id);
 
-      if (!shouldMoveAllItems &&
-          minSize &&
-          this._target.clientWidth <= minSize) {
-        break;
+      if (!shouldMoveAllItems && minSize) {
+        if (!targetWidth) {
+          let dwu = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+          targetWidth = Math.floor(dwu.getBoundsWithoutFlushing(this._target).width);
+        }
+        if (targetWidth <= minSize) {
+          break;
+        }
       }
 
       this._collapsed.delete(child.id);
       let beforeNodeIndex = placements.indexOf(child.id) + 1;
       // If this is a skipintoolbarset item, meaning it doesn't occur in the placements list,
       // we're inserting it at the end. This will mean first-in, first-out (more or less)
       // leading to as little change in order as possible.
       if (beforeNodeIndex == 0) {
@@ -4488,37 +4536,43 @@ OverflowableToolbar.prototype = {
         this._target.appendChild(child);
       }
       child.removeAttribute("cui-anchorid");
       child.removeAttribute("overflowedItem");
       CustomizableUIInternal.ensureButtonContextMenu(child, this._target);
       CustomizableUIInternal.notifyListeners("onWidgetUnderflow", child, this._target);
     }
 
-    let win = this._target.ownerGlobal;
     win.UpdateUrlbarSearchSplitterState();
 
     let collapsedWidgetIds = Array.from(this._collapsed.keys());
     if (collapsedWidgetIds.every(w => CustomizableUI.isSpecialWidget(w))) {
       this._toolbar.removeAttribute("overflowing");
     }
     if (this._addedListener && !this._collapsed.size) {
       CustomizableUI.removeListener(this);
       this._addedListener = false;
     }
   },
 
-  _onLazyResize() {
+  async _onLazyResize() {
     if (!this._enabled)
       return;
 
-    if (this._target.scrollLeftMin != this._target.scrollLeftMax) {
+    let win = this._target.ownerGlobal;
+    let [min, max, targetWidth] = await win.promiseDocumentFlushed(() => {
+      return [this._target.scrollLeftMin, this._target.scrollLeftMax, this._target.clientWidth];
+    });
+    if (win.closed) {
+      return;
+    }
+    if (min != max) {
       this.onOverflow();
     } else {
-      this._moveItemsBackToTheirOrigin();
+      this._moveItemsBackToTheirOrigin(false, targetWidth);
     }
   },
 
   _disable() {
     this._enabled = false;
     this._moveItemsBackToTheirOrigin(true);
     if (this._lazyResizeHandler) {
       this._lazyResizeHandler.disarm();
@@ -4603,17 +4657,17 @@ OverflowableToolbar.prototype = {
     } else if (aNode.previousSibling) {
       // but if it still is, it must have changed places. Bookkeep:
       let prevId = aNode.previousSibling.id;
       let minSize = this._collapsed.get(prevId);
       this._collapsed.set(aNode.id, minSize);
     } else {
       // If it's now the first item in the overflow list,
       // maybe we can return it:
-      this._moveItemsBackToTheirOrigin();
+      this._moveItemsBackToTheirOrigin(false);
     }
   },
 
   findOverflowedInsertionPoints(aNode) {
     let newNodeCanOverflow = aNode.getAttribute("overflows") != "false";
     let areaId = this._toolbar.id;
     let placements = gPlacements.get(areaId);
     let nodeIndex = placements.indexOf(aNode.id);
--- a/browser/components/customizableui/test/browser_976792_insertNodeInWindow.js
+++ b/browser/components/customizableui/test/browser_976792_insertNodeInWindow.js
@@ -221,17 +221,24 @@ add_task(async function() {
   }
 
   for (let id of widgetIds) {
     document.getElementById(id).style.minWidth = "200px";
   }
 
   let originalWindowWidth = window.outerWidth;
   window.resizeTo(kForceOverflowWidthPx, window.outerHeight);
-  await waitForCondition(() => navbar.hasAttribute("overflowing"));
+  // Wait for all the widgets to overflow. We can't just wait for the
+  // `overflowing` attribute because we leave time for layout flushes
+  // inbetween, so it's possible for the timeout to run before the
+  // navbar has "settled"
+  await waitForCondition(() => {
+    return navbar.hasAttribute("overflowing") &&
+      navbar.customizationTarget.lastChild.getAttribute("overflows") == "false";
+  });
 
   // Find last widget that doesn't allow overflowing
   let nonOverflowing = navbar.customizationTarget.lastChild;
   is(nonOverflowing.getAttribute("overflows"), "false", "Last child is expected to not allow overflowing");
   isnot(nonOverflowing.getAttribute("skipintoolbarset"), "true", "Last child is expected to not be skipintoolbarset");
 
   let testWidgetId = kTestWidgetPrefix + 10;
   CustomizableUI.destroyWidget(testWidgetId);
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -713,16 +713,17 @@ BrowserGlue.prototype = {
       name: gBrowserBundle.GetStringFromName("darkTheme.name"),
       description: gBrowserBundle.GetStringFromName("darkTheme.description"),
       iconURL: "resource:///chrome/browser/content/browser/defaultthemes/dark.icon.svg",
       textcolor: "white",
       accentcolor: "black",
       popup: "#4a4a4f",
       popup_text: "rgb(249, 249, 250)",
       popup_border: "#27272b",
+      toolbar_field_text: "rgb(249, 249, 250)",
       author: vendorShortName,
     });
 
     Normandy.init();
 
     // Initialize the default l10n resource sources for L10nRegistry.
     let locales = Services.locale.getPackagedLocales();
     const appSource = new FileSource("app", locales, "resource://app/localization/{locale}/");
--- a/browser/components/places/content/browserPlacesViews.js
+++ b/browser/components/places/content/browserPlacesViews.js
@@ -1167,21 +1167,25 @@ PlacesToolbar.prototype = {
         // the overflow status of the toolbar.
         if (aEvent.target == aEvent.currentTarget) {
           this.updateNodesVisibility();
         }
         break;
       case "overflow":
         if (!this._isOverflowStateEventRelevant(aEvent))
           return;
+        // Avoid triggering overflow in containers if possible
+        aEvent.stopPropagation();
         this._onOverflow();
         break;
       case "underflow":
         if (!this._isOverflowStateEventRelevant(aEvent))
           return;
+        // Avoid triggering underflow in containers if possible
+        aEvent.stopPropagation();
         this._onUnderflow();
         break;
       case "TabOpen":
       case "TabClose":
         this.updateNodesVisibility();
         break;
       case "dragstart":
         this._onDragStart(aEvent);
--- a/browser/themes/shared/compacttheme.inc.css
+++ b/browser/themes/shared/compacttheme.inc.css
@@ -78,19 +78,15 @@ toolbar[brighttext] .toolbarbutton-1 {
 }
 
 /* URL bar and search bar*/
 #urlbar:not([focused="true"]),
 .searchbar-textbox:not([focused="true"]) {
   border-color: var(--chrome-nav-bar-controls-border-color);
 }
 
-#urlbar[pageproxystate="valid"] > #identity-box.verifiedIdentity > #identity-icon-labels:-moz-lwtheme-brighttext {
-  color: #30e60b;
-}
-
 #urlbar-zoom-button:-moz-lwtheme-brighttext:hover {
   background-color: rgba(255,255,255,.2);
 }
 
 #urlbar-zoom-button:-moz-lwtheme-brighttext:hover:active {
   background-color: rgba(255,255,255,.3);
 }
--- a/browser/themes/shared/identity-block/identity-block.inc.css
+++ b/browser/themes/shared/identity-block/identity-block.inc.css
@@ -25,16 +25,21 @@
 #identity-box[open=true] {
   background-color: hsla(0,0%,70%,.3);
   fill-opacity: .8;
 }
 
 #urlbar[pageproxystate="valid"] > #identity-box.verifiedIdentity > #identity-icon-labels {
   color: #058B00;
 }
+
+:root[lwt-toolbar-field-brighttext] #urlbar[pageproxystate="valid"] > #identity-box.verifiedIdentity > #identity-icon-labels {
+  color: #30e60b;
+}
+
 #urlbar[pageproxystate="valid"] > #identity-box.chromeUI > #identity-icon-labels {
 %ifdef MOZ_OFFICIAL_BRANDING
   color: rgb(229,115,0);
 %else
   color: inherit;
 %endif
 }
 
--- a/devtools/client/themes/images/shape-swatch.svg
+++ b/devtools/client/themes/images/shape-swatch.svg
@@ -1,30 +1,4 @@
 <!-- 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 width="16" height="16" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="context-fill #0b0b0b">
-    <defs>
-        <circle id="path-1" cx="9" cy="4" r="2"></circle>
-        <circle id="path-2" cx="2.5" cy="4" r="2"></circle>
-        <circle id="path-3" cx="2.5" cy="10.5" r="2"></circle>
-        <circle id="path-4" cx="12.5" cy="10.5" r="2"></circle>
-    </defs>
-    <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
-        <path d="M6.3,4 L2.83333333,10.5 L12.5,10.5 L12.5,4 L6.3,4 Z" stroke="context-fill" transform="translate(7.500000, 7.250000) scale(-1, 1) translate(-7.500000, -7.250000) "></path>
-        <g transform="translate(9.000000, 4.000000) scale(-1, 1) translate(-9.000000, -4.000000) ">
-            <use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-1"></use>
-            <circle stroke="context-fill" stroke-width="1" cx="9" cy="4" r="1.5"></circle>
-        </g>
-        <g transform="translate(2.500000, 4.000000) scale(-1, 1) translate(-2.500000, -4.000000) ">
-            <use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-2"></use>
-            <circle stroke="context-fill" stroke-width="1" cx="2.5" cy="4" r="1.5"></circle>
-        </g>
-        <g transform="translate(2.500000, 10.500000) scale(-1, 1) translate(-2.500000, -10.500000) ">
-            <use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-3"></use>
-            <circle stroke="context-fill" stroke-width="1" cx="2.5" cy="10.5" r="1.5"></circle>
-        </g>
-        <g transform="translate(12.500000, 10.500000) scale(-1, 1) translate(-12.500000, -10.500000) ">
-            <use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-4"></use>
-            <circle stroke="context-fill" stroke-width="1" cx="12.5" cy="10.5" r="1.5"></circle>
-        </g>
-    </g>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" stroke="context-stroke #0b0b0b"><defs><style>.cls-1{fill:none;}</style></defs><path class="cls-1" d="M2.5,5.55V9L2.12,9A1.5,1.5,0,1,0,4,10.88L4,10.5H11l.1.38a1.5,1.5,0,1,0,.89-1.77l-.42.17-2-3.8.36-.25A1.5,1.5,0,1,0,7.55,3.62L7.45,4H4L4,3.62A1.5,1.5,0,1,0,2.12,5.45Z"/><circle class="cls-1" cx="12.5" cy="10.5" r="1.5"/><circle class="cls-1" cx="2.5" cy="10.5" r="1.5"/><circle class="cls-1" cx="9" cy="4" r="1.5"/><circle class="cls-1" cx="2.5" cy="4" r="1.5"/></svg>
--- a/devtools/client/themes/rules.css
+++ b/devtools/client/themes/rules.css
@@ -460,21 +460,23 @@
 
 .ruleview-angleswatch {
   background: url("chrome://devtools/skin/images/angle-swatch.svg");
   background-size: 1em;
 }
 
 .ruleview-shapeswatch {
   background: url("chrome://devtools/skin/images/shape-swatch.svg");
-  -moz-context-properties: fill;
-  fill: var(--rule-shape-toggle-color);
+  -moz-context-properties: stroke;
+  stroke: var(--rule-shape-toggle-color);
   border-radius: 0;
   background-size: 110% 110%;
   box-shadow: none;
+  width: 1.45em;
+  height: 1.45em;
 }
 
 @media (min-resolution: 1.1dppx) {
   .ruleview-bezierswatch {
     background: url("chrome://devtools/skin/images/cubic-bezier-swatch@2x.png");
     background-size: 1em;
   }
 }
--- a/devtools/docs/tests/performance-tests.md
+++ b/devtools/docs/tests/performance-tests.md
@@ -101,32 +101,39 @@ It is written in Python and here are [th
 Compared to the other test suites, it isn't run on the cloud, but on dedicated hardware.
 This is to ensure performance numbers are stable over time and between two runs.
 Talos runs various types of tests. More specifically, DAMP is a [Page loader test](https://wiki.mozilla.org/Buildbot/Talos/Tests#Page_Load_Tests).
 The [source code](http://searchfox.org/mozilla-central/source/testing/talos/talos/tests/devtools/) for DAMP is also in mozilla-central.
 The [main script](http://searchfox.org/mozilla-central/source/testing/talos/talos/tests/devtools/addon/content/damp.js) contains the implementation of all the tests described in "What does it do?" paragraph.
 
 ## How to see the performance trends?
 
-Here is a couple of links to track performance of each panel over the last 60 days:
-* Inspector:
-  * [Cold](https://treeherder.mozilla.org/perf.html#/graphs?timerange=5184000&series=mozilla-central,1556628,1,1)
-  * [Simple](https://treeherder.mozilla.org/perf.html#/graphs?timerange=5184000&series=mozilla-central,1417971,1,1&series=mozilla-central,1417969,1,1&series=mozilla-central,1417966,1,1)
-  * [Complicated](https://treeherder.mozilla.org/perf.html#/graphs?timerange=5184000&series=mozilla-central,1418016,1,1&series=mozilla-central,1418020,1,1&series=mozilla-central,1418018,1,1)
-* Console:
-  * [Simple](https://treeherder.mozilla.org/perf.html#/graphs?timerange=5184000&series=mozilla-central,1417964,1,1&series=mozilla-central,1417960,1,1&series=mozilla-central,1417962,1,1)
-  * [Complicated](https://treeherder.mozilla.org/perf.html#/graphs?timerange=5184000&series=mozilla-central,1418014,1,1&series=mozilla-central,1418010,1,1&series=mozilla-central,1418012,1,1)
-* Debugger:
-  * [Simple](https://treeherder.mozilla.org/perf.html#/graphs?timerange=5184000&series=mozilla-central,1417977,1,1&series=mozilla-central,1417973,1,1&series=mozilla-central,1417975,1,1)
-  * [Complicated](https://treeherder.mozilla.org/perf.html#/graphs?timerange=5184000&series=mozilla-central,1418026,1,1&series=mozilla-central,1418022,1,1&series=mozilla-central,1418024,1,1)
-* Netmonitor:
-  * [Simple](https://treeherder.mozilla.org/perf.html#/graphs?timerange=5184000&series=mozilla-central,1417996,1,1&series=mozilla-central,1417992,1,1&series=mozilla-central,1417994,1,1&series=mozilla-central,1470289,1,1)
-  * [Complicated](https://treeherder.mozilla.org/perf.html#/graphs?timerange=5184000&series=mozilla-central,1418041,1,1&series=mozilla-central,1418039,1,1&series=mozilla-central,1418040,1,1&series=mozilla-central,1470290,1,1)
+You can find the dedicated performance dashboard for DevTools at http://firefox-dev.tools/performance-dashboard. You will find links to trend charts for various tools:
+* [Inspector dashboard](firefox-dev.tools/performance-dashboard/tools/inspector.html?days=60&filterstddev=true)
+* [Console dashboard](http://firefox-dev.tools/performance-dashboard/tools/console.html?days=60&filterstddev=true)
+* [Netmonitor dashboard](http://firefox-dev.tools/performance-dashboard/tools/netmonitor.html?days=60&filterstddev=true)
+* [Debugger dashboard](http://firefox-dev.tools/performance-dashboard/tools/debugger.html?days=60&filterstddev=true)
+
+Each tool page displays charts for all the subtests relevant for a given panel.
+
+Each circle on the chart is a push to mozilla-central. You can hover on a circle to see some additional information about the push, such as the date, the performance impact for the subtest, and the push id. Clicking on a circle will take you to the pushlog.
 
-On these graphs, each circle is a push on mozilla-central.
+Colored circles indicate that the push contains a change that was identified as having a performance impact. Those can be categorized as:
+- hardware: hardware change for the machines used to run Talos
+- platform: non-DevTools change that impacts DevTools performance
+- damp: test change in DAMP that impacts test results
+- devtools: identified DevTools change that introduced an improvement or regression
+
+This data is synchronized from a [shared Google doc](https://docs.google.com/spreadsheets/d/12Goo3vq-0X0_Ay-J6gfV56pUB8GC0Nl62I4p8G-UsEA/edit#gid=0).
+
+There is a PerfHerder link on each chart that will take you to the PerfHerder page corresponding to this subtest.
+
+## How to use PerfHerder charts
+
+On PerfHerder charts, each circle is a push on mozilla-central.
 When you see a spike or a drop, you can try to identify the patch that relates to it by clicking the circles.
 It will show a black popup. Then click on the changeset hash like "cb717386aec8" and you will get a mercurial changelog.
 Then it is up to you to read the changelog and see which changeset may have hit the performance.
 
 For example, open [this page](https://treeherder.mozilla.org/perf.html#/graphs?timerange=31536000&series=mozilla-central,1417969,1,1&series=mozilla-central,1417971,1,1&series=mozilla-central,1417966,1,1&highlightedRevisions=a06f92099a5d&zoom=1482734645161.3916,1483610598216.4773,594.756508587898,969.2883437938906).
 This is tracking inspector opening performance against the "Simple" page.
 ![Perfherder graphs](regression-graph.png)
 
--- a/devtools/server/actors/chrome.js
+++ b/devtools/server/actors/chrome.js
@@ -5,16 +5,20 @@
 "use strict";
 
 const { Ci } = require("chrome");
 const Services = require("Services");
 const { DebuggerServer } = require("../main");
 const { getChildDocShells, TabActor } = require("./tab");
 const makeDebugger = require("./utils/make-debugger");
 
+const { extend } = require("devtools/shared/extend");
+const { ActorClassWithSpec, Actor } = require("devtools/shared/protocol");
+const { tabSpec } = require("devtools/shared/specs/tab");
+
 /**
  * Creates a TabActor for debugging all the chrome content in the
  * current process. Most of the implementation is inherited from TabActor.
  * ChromeActor is a child of RootActor, it can be instanciated via
  * RootActor.getProcess request.
  * ChromeActor exposes all tab actors via its form() request, like TabActor.
  *
  * History lecture:
@@ -25,17 +29,27 @@ const makeDebugger = require("./utils/ma
  * So we are now exposing a process actor that offers the same API as TabActor
  * by inheriting its functionality.
  * Global actors are now only the actors that are meant to be global,
  * and are no longer related to any specific scope/document.
  *
  * @param connection DebuggerServerConnection
  *        The connection to the client.
  */
-function ChromeActor(connection) {
+
+/**
+ * Protocol.js expects only the prototype object, and does not maintain the prototype
+ * chain when it constructs the ActorClass. For this reason we are using `extend` to
+ * maintain the properties of TabActor.prototype
+ * */
+
+const chromePrototype = extend({}, TabActor.prototype);
+
+chromePrototype.initialize = function(connection) {
+  Actor.prototype.initialize.call(this, connection);
   TabActor.call(this, connection);
 
   // This creates a Debugger instance for chrome debugging all globals.
   this.makeDebugger = makeDebugger.bind(null, {
     findDebuggees: dbg => dbg.findAllGlobals(),
     shouldAddNewGlobalAsDebuggee: () => true
   });
 
@@ -64,62 +78,57 @@ function ChromeActor(connection) {
   // On XPCShell, there is no window/docshell
   let docShell = window ? window.QueryInterface(Ci.nsIInterfaceRequestor)
                                 .getInterface(Ci.nsIDocShell)
                         : null;
   Object.defineProperty(this, "docShell", {
     value: docShell,
     configurable: true
   });
-}
-exports.ChromeActor = ChromeActor;
+};
 
-ChromeActor.prototype = Object.create(TabActor.prototype);
-
-ChromeActor.prototype.constructor = ChromeActor;
-
-ChromeActor.prototype.isRootActor = true;
+chromePrototype.isRootActor = true;
 
 /**
  * Getter for the list of all docshells in this tabActor
  * @return {Array}
  */
-Object.defineProperty(ChromeActor.prototype, "docShells", {
+Object.defineProperty(chromePrototype, "docShells", {
   get: function() {
     // Iterate over all top-level windows and all their docshells.
     let docShells = [];
     let e = Services.ww.getWindowEnumerator();
     while (e.hasMoreElements()) {
       let window = e.getNext();
       let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
                            .getInterface(Ci.nsIWebNavigation)
                            .QueryInterface(Ci.nsIDocShell);
       docShells = docShells.concat(getChildDocShells(docShell));
     }
 
     return docShells;
   }
 });
 
-ChromeActor.prototype.observe = function(subject, topic, data) {
+chromePrototype.observe = function(subject, topic, data) {
   TabActor.prototype.observe.call(this, subject, topic, data);
   if (!this.attached) {
     return;
   }
 
   subject.QueryInterface(Ci.nsIDocShell);
 
   if (topic == "chrome-webnavigation-create") {
     this._onDocShellCreated(subject);
   } else if (topic == "chrome-webnavigation-destroy") {
     this._onDocShellDestroy(subject);
   }
 };
 
-ChromeActor.prototype._attach = function() {
+chromePrototype._attach = function() {
   if (this.attached) {
     return false;
   }
 
   TabActor.prototype._attach.call(this);
 
   // Listen for any new/destroyed chrome docshell
   Services.obs.addObserver(this, "chrome-webnavigation-create");
@@ -135,17 +144,17 @@ ChromeActor.prototype._attach = function
     if (docShell == this.docShell) {
       continue;
     }
     this._progressListener.watch(docShell);
   }
   return undefined;
 };
 
-ChromeActor.prototype._detach = function() {
+chromePrototype._detach = function() {
   if (!this.attached) {
     return false;
   }
 
   Services.obs.removeObserver(this, "chrome-webnavigation-create");
   Services.obs.removeObserver(this, "chrome-webnavigation-destroy");
 
   // Iterate over all top-level windows.
@@ -165,34 +174,38 @@ ChromeActor.prototype._detach = function
   return undefined;
 };
 
 /* ThreadActor hooks. */
 
 /**
  * Prepare to enter a nested event loop by disabling debuggee events.
  */
-ChromeActor.prototype.preNest = function() {
+chromePrototype.preNest = function() {
   // Disable events in all open windows.
   let e = Services.wm.getEnumerator(null);
   while (e.hasMoreElements()) {
     let win = e.getNext();
     let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
                          .getInterface(Ci.nsIDOMWindowUtils);
     windowUtils.suppressEventHandling(true);
     windowUtils.suspendTimeouts();
   }
 };
 
 /**
  * Prepare to exit a nested event loop by enabling debuggee events.
  */
-ChromeActor.prototype.postNest = function(nestData) {
+chromePrototype.postNest = function(nestData) {
   // Enable events in all open windows.
   let e = Services.wm.getEnumerator(null);
   while (e.hasMoreElements()) {
     let win = e.getNext();
     let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
                          .getInterface(Ci.nsIDOMWindowUtils);
     windowUtils.resumeTimeouts();
     windowUtils.suppressEventHandling(false);
   }
 };
+
+chromePrototype.typeName = "Chrome";
+exports.chromePrototype = chromePrototype;
+exports.ChromeActor = ActorClassWithSpec(tabSpec, chromePrototype);
--- a/devtools/server/actors/tab.js
+++ b/devtools/server/actors/tab.js
@@ -606,17 +606,17 @@ TabActor.prototype = {
   },
 
   _unwatchDocShell(docShell) {
     if (this._progressListener) {
       this._progressListener.unwatch(docShell);
     }
   },
 
-  onSwitchToFrame(request) {
+  switchToFrame(request) {
     let windowId = request.windowId;
     let win;
 
     try {
       win = Services.wm.getOuterWindowWithId(windowId);
     } catch (e) {
       // ignore
     }
@@ -628,22 +628,22 @@ TabActor.prototype = {
     }
 
     // Reply first before changing the document
     DevToolsUtils.executeSoon(() => this._changeTopLevelDocument(win));
 
     return {};
   },
 
-  onListFrames(request) {
+  listFrames(request) {
     let windows = this._docShellsToWindows(this.docShells);
     return { frames: windows };
   },
 
-  onListWorkers(request) {
+  listWorkers(request) {
     if (!this.attached) {
       return { error: "wrongState" };
     }
 
     if (this._workerActorList === null) {
       this._workerActorList = new WorkerActorList(this.conn, {
         type: Ci.nsIWorkerDebugger.TYPE_DEDICATED,
         window: this.window
@@ -664,17 +664,17 @@ TabActor.prototype = {
 
       return {
         "from": this.actorID,
         "workers": actors.map((actor) => actor.form())
       };
     });
   },
 
-  onLogInPage(request) {
+  logInPage(request) {
     let {text, category, flags} = request;
     let scriptErrorClass = Cc["@mozilla.org/scripterror;1"];
     let scriptError = scriptErrorClass.createInstance(Ci.nsIScriptError);
     scriptError.initWithWindowID(text, null, null, 0, 0, flags,
                                  category, getInnerId(this.window));
     Services.console.logMessage(scriptError);
     return {};
   },
@@ -934,86 +934,86 @@ TabActor.prototype = {
     this.conn.send({ from: this.actorID,
                      type: "tabDetached" });
 
     return true;
   },
 
   // Protocol Request Handlers
 
-  onAttach(request) {
+  attach(request) {
     if (this.exited) {
       return { type: "exited" };
     }
 
     this._attach();
 
     return {
       type: "tabAttached",
       threadActor: this.threadActor.actorID,
       cacheDisabled: this._getCacheDisabled(),
       javascriptEnabled: this._getJavascriptEnabled(),
       traits: this.traits,
     };
   },
 
-  onDetach(request) {
+  detach(request) {
     if (!this._detach()) {
       return { error: "wrongState" };
     }
 
     return { type: "detached" };
   },
 
   /**
    * Bring the tab's window to front.
    */
-  onFocus() {
+  focus() {
     if (this.window) {
       this.window.focus();
     }
     return {};
   },
 
   /**
    * Reload the page in this tab.
    */
-  onReload(request) {
+  reload(request) {
     let force = request && request.options && request.options.force;
     // Wait a tick so that the response packet can be dispatched before the
     // subsequent navigation event packet.
     Services.tm.dispatchToMainThread(DevToolsUtils.makeInfallible(() => {
       // This won't work while the browser is shutting down and we don't really
       // care.
       if (Services.startup.shuttingDown) {
         return;
       }
       this.webNavigation.reload(force ?
         Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE :
         Ci.nsIWebNavigation.LOAD_FLAGS_NONE);
-    }, "TabActor.prototype.onReload's delayed body"));
+    }, "TabActor.prototype.reload's delayed body"));
     return {};
   },
 
   /**
    * Navigate this tab to a new location
    */
-  onNavigateTo(request) {
+  navigateTo(request) {
     // Wait a tick so that the response packet can be dispatched before the
     // subsequent navigation event packet.
     Services.tm.dispatchToMainThread(DevToolsUtils.makeInfallible(() => {
       this.window.location = request.url;
-    }, "TabActor.prototype.onNavigateTo's delayed body"));
+    }, "TabActor.prototype.navigateTo's delayed body"));
     return {};
   },
 
   /**
    * Reconfigure options.
    */
-  onReconfigure(request) {
+  reconfigure(request) {
     let options = request.options || {};
 
     if (!this.docShell) {
       // The tab is already closed.
       return {};
     }
     this._toggleDevToolsSettings(options);
 
@@ -1073,17 +1073,17 @@ TabActor.prototype = {
     }
 
     // Reload if:
     //  - there's an explicit `performReload` flag and it's true
     //  - there's no `performReload` flag, but it makes sense to do so
     let hasExplicitReloadFlag = "performReload" in options;
     if ((hasExplicitReloadFlag && options.performReload) ||
        (!hasExplicitReloadFlag && reload)) {
-      this.onReload();
+      this.reload();
     }
   },
 
   /**
    * Opposite of the _toggleDevToolsSettings method, that reset document state
    * when closing the toolbox.
    */
   _restoreDocumentSettings() {
@@ -1459,27 +1459,27 @@ TabActor.prototype = {
     }
   },
 };
 
 /**
  * The request types this actor can handle.
  */
 TabActor.prototype.requestTypes = {
-  "attach": TabActor.prototype.onAttach,
-  "detach": TabActor.prototype.onDetach,
-  "focus": TabActor.prototype.onFocus,
-  "reload": TabActor.prototype.onReload,
-  "navigateTo": TabActor.prototype.onNavigateTo,
-  "reconfigure": TabActor.prototype.onReconfigure,
+  "attach": TabActor.prototype.attach,
+  "detach": TabActor.prototype.detach,
+  "focus": TabActor.prototype.focus,
+  "reload": TabActor.prototype.reload,
+  "navigateTo": TabActor.prototype.navigateTo,
+  "reconfigure": TabActor.prototype.reconfigure,
   "ensureCSSErrorReportingEnabled": TabActor.prototype.ensureCSSErrorReportingEnabled,
-  "switchToFrame": TabActor.prototype.onSwitchToFrame,
-  "listFrames": TabActor.prototype.onListFrames,
-  "listWorkers": TabActor.prototype.onListWorkers,
-  "logInPage": TabActor.prototype.onLogInPage,
+  "switchToFrame": TabActor.prototype.switchToFrame,
+  "listFrames": TabActor.prototype.listFrames,
+  "listWorkers": TabActor.prototype.listWorkers,
+  "logInPage": TabActor.prototype.logInPage,
 };
 
 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/actors/webextension.js
+++ b/devtools/server/actors/webextension.js
@@ -1,23 +1,25 @@
 /* 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 { extend } = require("devtools/shared/extend");
 const { Ci, Cu, Cc } = require("chrome");
 const Services = require("Services");
 
-const { ChromeActor } = require("./chrome");
+const { ChromeActor, chromePrototype } = require("./chrome");
 const makeDebugger = require("./utils/make-debugger");
+const { ActorClassWithSpec } = require("devtools/shared/protocol");
+const { tabSpec } = require("devtools/shared/specs/tab");
 
 loader.lazyRequireGetter(this, "unwrapDebuggerObjectGlobal", "devtools/server/actors/thread", true);
 loader.lazyRequireGetter(this, "ChromeUtils");
-
 const FALLBACK_DOC_MESSAGE = "Your addon does not have any document opened yet.";
 
 /**
  * Creates a TabActor for debugging all the contexts associated to a target WebExtensions
  * add-on running in a child extension process.
  * Most of the implementation is inherited from ChromeActor (which inherits most of its
  * implementation from TabActor).
  * WebExtensionChildActor is created by a WebExtensionParentActor counterpart, when its
@@ -47,19 +49,21 @@ const FALLBACK_DOC_MESSAGE = "Your addon
  * @param {nsIMessageSender} chromeGlobal.
  *        The chromeGlobal where this actor has been injected by the
  *        DebuggerServer.connectToFrame method.
  * @param {string} prefix
  *        the custom RDP prefix to use.
  * @param {string} addonId
  *        the addonId of the target WebExtension.
  */
-function WebExtensionChildActor(conn, chromeGlobal, prefix, addonId) {
-  ChromeActor.call(this, conn);
+
+const webExtensionChildPrototype = extend({}, chromePrototype);
 
+webExtensionChildPrototype.initialize = function(conn, chromeGlobal, prefix, addonId) {
+  chromePrototype.initialize.call(this, conn);
   this._chromeGlobal = chromeGlobal;
   this._prefix = prefix;
   this.id = addonId;
 
   // Redefine the messageManager getter to return the chromeGlobal
   // as the messageManager for this actor (which is the browser XUL
   // element used by the parent actor running in the main process to
   // connect to the extension process).
@@ -97,33 +101,29 @@ function WebExtensionChildActor(conn, ch
 
   // Try to discovery an existent extension page to attach (which will provide the initial
   // URL shown in the window tittle when the addon debugger is opened).
   let extensionWindow = this._searchForExtensionWindow();
 
   if (extensionWindow) {
     this._setWindow(extensionWindow);
   }
-}
-exports.WebExtensionChildActor = WebExtensionChildActor;
+};
 
-WebExtensionChildActor.prototype = Object.create(ChromeActor.prototype);
-
-WebExtensionChildActor.prototype.actorPrefix = "webExtension";
-WebExtensionChildActor.prototype.constructor = WebExtensionChildActor;
+webExtensionChildPrototype.typeName = "webExtension";
 
 // NOTE: This is needed to catch in the webextension webconsole all the
 // errors raised by the WebExtension internals that are not currently
 // associated with any window.
-WebExtensionChildActor.prototype.isRootActor = true;
+webExtensionChildPrototype.isRootActor = true;
 
 /**
  * Called when the actor is removed from the connection.
  */
-WebExtensionChildActor.prototype.exit = function() {
+webExtensionChildPrototype.exit = function() {
   if (this._chromeGlobal) {
     let chromeGlobal = this._chromeGlobal;
     this._chromeGlobal = null;
 
     chromeGlobal.removeMessageListener("debug:webext_parent_exit", this._onParentExit);
 
     chromeGlobal.sendAsyncMessage("debug:webext_child_exit", {
       actor: this.actorID
@@ -133,17 +133,17 @@ WebExtensionChildActor.prototype.exit = 
   this.addon = null;
   this.id = null;
 
   return ChromeActor.prototype.exit.apply(this);
 };
 
 // Private helpers.
 
-WebExtensionChildActor.prototype._createFallbackWindow = function() {
+webExtensionChildPrototype._createFallbackWindow = function() {
   if (this.fallbackWindow) {
     // Skip if there is already an existent fallback window.
     return;
   }
 
   // Create an empty hidden window as a fallback (e.g. the background page could be
   // not defined for the target add-on or not yet when the actor instance has been
   // created).
@@ -152,48 +152,48 @@ WebExtensionChildActor.prototype._create
   // Save the reference to the fallback DOMWindow.
   this.fallbackWindow = this.fallbackWebNav.QueryInterface(Ci.nsIInterfaceRequestor)
                                            .getInterface(Ci.nsIDOMWindow);
 
   // Insert the fallback doc message.
   this.fallbackWindow.document.body.innerText = FALLBACK_DOC_MESSAGE;
 };
 
-WebExtensionChildActor.prototype._destroyFallbackWindow = function() {
+webExtensionChildPrototype._destroyFallbackWindow = function() {
   if (this.fallbackWebNav) {
     // Explicitly close the fallback windowless browser to prevent it to leak
     // (and to prevent it to freeze devtools xpcshell tests).
     this.fallbackWebNav.loadURI("about:blank", 0, null, null, null);
     this.fallbackWebNav.close();
 
     this.fallbackWebNav = null;
     this.fallbackWindow = null;
   }
 };
 
 // Discovery an extension page to use as a default target window.
 // NOTE: This currently fail to discovery an extension page running in a
 // windowless browser when running in non-oop mode, and the background page
 // is set later using _onNewExtensionWindow.
-WebExtensionChildActor.prototype._searchForExtensionWindow = function() {
+webExtensionChildPrototype._searchForExtensionWindow = function() {
   let e = Services.ww.getWindowEnumerator(null);
   while (e.hasMoreElements()) {
     let window = e.getNext();
 
     if (window.document.nodePrincipal.addonId == this.id) {
       return window;
     }
   }
 
   return undefined;
 };
 
 // Customized ChromeActor/TabActor hooks.
 
-WebExtensionChildActor.prototype._onDocShellDestroy = function(docShell) {
+webExtensionChildPrototype._onDocShellDestroy = function(docShell) {
   // Stop watching this docshell (the unwatch() method will check if we
   // started watching it before).
   this._unwatchDocShell(docShell);
 
   // Let the _onDocShellDestroy notify that the docShell has been destroyed.
   let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
         .getInterface(Ci.nsIWebProgress);
   this._notifyDocShellDestroy(webProgress);
@@ -202,23 +202,23 @@ WebExtensionChildActor.prototype._onDocS
   // currently attached, switch to the fallback window
   if (this.attached && docShell == this.docShell) {
     // Creates a fallback window if it doesn't exist yet.
     this._createFallbackWindow();
     this._changeTopLevelDocument(this.fallbackWindow);
   }
 };
 
-WebExtensionChildActor.prototype._onNewExtensionWindow = function(window) {
+webExtensionChildPrototype._onNewExtensionWindow = function(window) {
   if (!this.window || this.window === this.fallbackWindow) {
     this._changeTopLevelDocument(window);
   }
 };
 
-WebExtensionChildActor.prototype._attach = function() {
+webExtensionChildPrototype._attach = function() {
   // NOTE: we need to be sure that `this.window` can return a
   // window before calling the ChromeActor.onAttach, or the TabActor
   // will not be subscribed to the child doc shell updates.
 
   if (!this.window || this.window.document.nodePrincipal.addonId !== this.id) {
     // Discovery an existent extension page to attach.
     let extensionWindow = this._searchForExtensionWindow();
 
@@ -229,28 +229,28 @@ WebExtensionChildActor.prototype._attach
       this._setWindow(extensionWindow);
     }
   }
 
   // Call ChromeActor's _attach to listen for any new/destroyed chrome docshell
   ChromeActor.prototype._attach.apply(this);
 };
 
-WebExtensionChildActor.prototype._detach = function() {
+webExtensionChildPrototype._detach = function() {
   // Call ChromeActor's _detach to unsubscribe new/destroyed chrome docshell listeners.
   ChromeActor.prototype._detach.apply(this);
 
   // Stop watching for new extension windows.
   this._destroyFallbackWindow();
 };
 
 /**
  * Return the json details related to a docShell.
  */
-WebExtensionChildActor.prototype._docShellToWindow = function(docShell) {
+webExtensionChildPrototype._docShellToWindow = function(docShell) {
   const baseWindowDetails = ChromeActor.prototype._docShellToWindow.call(this, docShell);
 
   let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
                             .getInterface(Ci.nsIWebProgress);
   let window = webProgress.DOMWindow;
 
   // Collect the addonID from the document origin attributes and its sameType top level
   // frame.
@@ -265,44 +265,44 @@ WebExtensionChildActor.prototype._docShe
     addonID,
     sameTypeRootAddonID,
   });
 };
 
 /**
  * Return an array of the json details related to an array/iterator of docShells.
  */
-WebExtensionChildActor.prototype._docShellsToWindows = function(docshells) {
+webExtensionChildPrototype._docShellsToWindows = function(docshells) {
   return ChromeActor.prototype._docShellsToWindows.call(this, docshells)
                     .filter(windowDetails => {
                       // Filter the docShells based on the addon id of the window or
                       // its sameType top level frame.
                       return windowDetails.addonID === this.id ||
                              windowDetails.sameTypeRootAddonID === this.id;
                     });
 };
 
-WebExtensionChildActor.prototype.isExtensionWindow = function(window) {
+webExtensionChildPrototype.isExtensionWindow = function(window) {
   return window.document.nodePrincipal.addonId == this.id;
 };
 
-WebExtensionChildActor.prototype.isExtensionWindowDescendent = function(window) {
+webExtensionChildPrototype.isExtensionWindowDescendent = function(window) {
   // Check if the source is coming from a descendant docShell of an extension window.
   let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
                        .getInterface(Ci.nsIDocShell);
   let rootWin = docShell.sameTypeRootTreeItem.QueryInterface(Ci.nsIInterfaceRequestor)
                                              .getInterface(Ci.nsIDOMWindow);
   return this.isExtensionWindow(rootWin);
 };
 
 /**
  * Return true if the given source is associated with this addon and should be
  * added to the visible sources (retrieved and used by the webbrowser actor module).
  */
-WebExtensionChildActor.prototype._allowSource = function(source) {
+webExtensionChildPrototype._allowSource = function(source) {
   // Use the source.element to detect the allowed source, if any.
   if (source.element) {
     let domEl = unwrapDebuggerObjectGlobal(source.element);
     return (this.isExtensionWindow(domEl.ownerGlobal) ||
             this.isExtensionWindowDescendent(domEl.ownerGlobal));
   }
 
   // Fallback to check the uri if there is no source.element associated to the source.
@@ -339,17 +339,17 @@ WebExtensionChildActor.prototype._allowS
     return false;
   }
 };
 
 /**
  * Return true if the given global is associated with this addon and should be
  * added as a debuggee, false otherwise.
  */
-WebExtensionChildActor.prototype._shouldAddNewGlobalAsDebuggee = function(newGlobal) {
+webExtensionChildPrototype._shouldAddNewGlobalAsDebuggee = function(newGlobal) {
   const global = unwrapDebuggerObjectGlobal(newGlobal);
 
   if (global instanceof Ci.nsIDOMWindow) {
     try {
       global.document;
     } catch (e) {
       // The global might be a sandbox with a window object in its proto chain. If the
       // window navigated away since the sandbox was created, it can throw a security
@@ -382,15 +382,17 @@ WebExtensionChildActor.prototype._should
     // Unable to retrieve the sandbox metadata.
   }
 
   return false;
 };
 
 // Handlers for the messages received from the parent actor.
 
-WebExtensionChildActor.prototype._onParentExit = function(msg) {
+webExtensionChildPrototype._onParentExit = function(msg) {
   if (msg.json.actor !== this.actorID) {
     return;
   }
 
   this.exit();
 };
+
+exports.WebExtensionChildActor = ActorClassWithSpec(tabSpec, webExtensionChildPrototype);
--- a/devtools/shared/base-loader.js
+++ b/devtools/shared/base-loader.js
@@ -49,17 +49,17 @@ function isRelative(id) {
   return id.startsWith(".");
 }
 
 function sourceURI(uri) {
   return String(uri).split(" -> ").pop();
 }
 
 function isntLoaderFrame(frame) {
-  return frame.fileName !== __URI__;
+  return frame.fileName !== __URI__ && !frame.fileName.endsWith("/browser-loader.js");
 }
 
 function parseURI(uri) {
   return String(uri).split(" -> ").pop();
 }
 
 function parseStack(stack) {
   let lines = String(stack).split("\n");
--- a/devtools/shared/protocol.js
+++ b/devtools/shared/protocol.js
@@ -922,27 +922,35 @@ Pool.prototype = extend(EventEmitter.pro
    */
   cleanup: function() {
     this.destroy();
   }
 });
 exports.Pool = Pool;
 
 /**
+ * Keep track of which actorSpecs have been created. If a replica of a spec
+ * is created, it can be caught, and specs which inherit from other specs will
+ * not overwrite eachother.
+ */
+var actorSpecs = new WeakMap();
+
+/**
  * An actor in the actor tree.
  *
  * @param optional conn
  *   Either a DebuggerServerConnection or a DebuggerClient.  Must have
  *   addActorPool, removeActorPool, and poolFor.
  *   conn can be null if the subclass provides a conn property.
  * @constructor
  */
 var Actor = function(conn) {
   Pool.call(this, conn);
 
+  this._actorSpec = actorSpecs.get(Object.getPrototypeOf(this));
   // Forward events to the connection.
   if (this._actorSpec && this._actorSpec.events) {
     for (let [name, request] of this._actorSpec.events.entries()) {
       this.on(name, (...args) => {
         this._sendEvent(name, request, ...args);
       });
     }
   }
@@ -1105,20 +1113,16 @@ var generateActorSpec = function(actorDe
 };
 exports.generateActorSpec = generateActorSpec;
 
 /**
  * Generates request handlers as described by the given actor specification on
  * the given actor prototype. Returns the actor prototype.
  */
 var generateRequestHandlers = function(actorSpec, actorProto) {
-  if (actorProto._actorSpec) {
-    throw new Error("actorProto called twice on the same actor prototype!");
-  }
-
   actorProto.typeName = actorSpec.typeName;
 
   // Generate request handlers for each method definition
   actorProto.requestTypes = Object.create(null);
   actorSpec.methods.forEach(spec => {
     let handler = function(packet, conn) {
       try {
         let args;
@@ -1169,18 +1173,16 @@ var generateRequestHandlers = function(a
           return p.then(() => this.writeError(e));
         });
       }
     };
 
     actorProto.requestTypes[spec.request.type] = handler;
   });
 
-  actorProto._actorSpec = actorSpec;
-
   return actorProto;
 };
 
 /**
  * Create an actor class for the given actor specification and prototype.
  *
  * @param object actorSpec
  *    The actor specification. Must have a 'typeName' property.
@@ -1196,16 +1198,18 @@ var ActorClassWithSpec = function(actorS
   // Existing Actors are relying on the initialize instead of constructor methods.
   let cls = function() {
     let instance = Object.create(cls.prototype);
     instance.initialize.apply(instance, arguments);
     return instance;
   };
   cls.prototype = extend(Actor.prototype, generateRequestHandlers(actorSpec, actorProto));
 
+  actorSpecs.set(cls.prototype, actorSpec);
+
   return cls;
 };
 exports.ActorClassWithSpec = ActorClassWithSpec;
 
 /**
  * Base class for client-side actor fronts.
  *
  * @param optional conn
--- a/devtools/shared/specs/index.js
+++ b/devtools/shared/specs/index.js
@@ -200,16 +200,21 @@ const Types = exports.__TypesForTests = 
     front: "devtools/shared/fronts/stylesheets",
   },
   {
     types: ["symbolIterator"],
     spec: "devtools/shared/specs/symbol-iterator",
     front: null,
   },
   {
+    types: ["tab"],
+    spec: "devtools/shared/specs/tab",
+    front: null,
+  },
+  {
     types: ["timeline"],
     spec: "devtools/shared/specs/timeline",
     front: "devtools/shared/fronts/timeline",
   },
   {
     types: ["audionode", "webaudio"],
     spec: "devtools/shared/specs/webaudio",
     front: "devtools/shared/fronts/webaudio",
--- a/devtools/shared/specs/moz.build
+++ b/devtools/shared/specs/moz.build
@@ -36,15 +36,16 @@ DevToolsModules(
     'reflow.js',
     'script.js',
     'source.js',
     'storage.js',
     'string.js',
     'styles.js',
     'stylesheets.js',
     'symbol-iterator.js',
+    'tab.js',
     'timeline.js',
     'webaudio.js',
     'webextension-inspected-window.js',
     'webextension-parent.js',
     'webgl.js',
     'worker.js'
 )
new file mode 100644
--- /dev/null
+++ b/devtools/shared/specs/tab.js
@@ -0,0 +1,111 @@
+/* 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 {types, generateActorSpec, RetVal, Option} = require("devtools/shared/protocol");
+
+types.addDictType("tab.attach", {
+  type: "string",
+  threadActor: "number",
+  cacheDisabled: "boolean",
+  javascriptEnabled: "boolean",
+  traits: "json"
+});
+
+types.addDictType("tab.detach", {
+  error: "nullable:string",
+  type: "nullable:string"
+});
+
+types.addDictType("tab.switchtoframe", {
+  error: "nullable:string",
+  message: "nullable:string"
+});
+
+types.addDictType("tab.listframes", {
+  frames: "array:tab.window"
+});
+
+types.addDictType("tab.window", {
+  id: "string",
+  parentID: "nullable:string",
+  url: "string",
+  title: "string"
+});
+
+types.addDictType("tab.workers", {
+  error: "nullable:string"
+});
+
+types.addDictType("tab.reload", {
+  force: "boolean"
+});
+
+types.addDictType("tab.reconfigure", {
+  javascriptEnabled: "nullable:boolean",
+  cacheDisabled: "nullable:boolean",
+  serviceWorkersTestingEnabled: "nullable:boolean",
+  performReload: "nullable:boolean"
+});
+
+const tabSpec = generateActorSpec({
+  typeName: "tab",
+
+  methods: {
+    attach: {
+      request: {},
+      response: RetVal("tab.attach")
+    },
+    detach: {
+      request: {},
+      response: RetVal("tab.detach")
+    },
+    focus: {
+      request: {},
+      response: {}
+    },
+    reload: {
+      request: {
+        options: Option(0, "tab.reload"),
+      },
+      response: {}
+    },
+    navigateTo: {
+      request: {
+        url: Option(0, "string"),
+      },
+      response: {}
+    },
+    reconfigure: {
+      request: {
+        options: Option(0, "tab.reconfigure")
+      },
+      response: {}
+    },
+    switchToFrame: {
+      request: {
+        windowId: Option(0, "string")
+      },
+      response: RetVal("tab.switchtoframe")
+    },
+    listFrames: {
+      request: {},
+      response: RetVal("tab.listframes")
+    },
+    listWorkers: {
+      request: {},
+      response: RetVal("tab.workers")
+    },
+    logInPage: {
+      request: {
+        text: Option(0, "string"),
+        category: Option(0, "string"),
+        flags: Option(0, "string")
+      },
+      response: {}
+    }
+  },
+});
+
+exports.tabSpec = tabSpec;
--- a/media/mp4parse-rust/mp4parse/src/lib.rs
+++ b/media/mp4parse-rust/mp4parse/src/lib.rs
@@ -830,34 +830,40 @@ fn read_trak<T: Read>(f: &mut BMFFBox<T>
 }
 
 fn read_edts<T: Read>(f: &mut BMFFBox<T>, track: &mut Track) -> Result<()> {
     let mut iter = f.box_iter();
     while let Some(mut b) = iter.next_box()? {
         match b.head.name {
             BoxType::EditListBox => {
                 let elst = read_elst(&mut b)?;
+                if elst.edits.len() < 1 {
+                    debug!("empty edit list");
+                    continue;
+                }
                 let mut empty_duration = 0;
                 let mut idx = 0;
-                if elst.edits.len() > 2 {
-                    return Err(Error::Unsupported("more than two edits"));
-                }
                 if elst.edits[idx].media_time == -1 {
+                    if elst.edits.len() < 2 {
+                        debug!("expected additional edit, ignoring edit list");
+                        continue;
+                    }
                     empty_duration = elst.edits[idx].segment_duration;
-                    if elst.edits.len() < 2 {
-                        return Err(Error::InvalidData("expected additional edit"));
-                    }
                     idx += 1;
                 }
                 track.empty_duration = Some(MediaScaledTime(empty_duration));
-                if elst.edits[idx].media_time < 0 {
-                    return Err(Error::InvalidData("unexpected negative media time in edit"));
+                let media_time = elst.edits[idx].media_time;
+                if media_time < 0 {
+                    debug!("unexpected negative media time in edit");
                 }
-                track.media_time = Some(TrackScaledTime::<u64>(elst.edits[idx].media_time as u64,
+                track.media_time = Some(TrackScaledTime::<u64>(std::cmp::max(0, media_time) as u64,
                                                         track.id));
+                if elst.edits.len() > 2 {
+                    debug!("ignoring edit list with {} entries", elst.edits.len());
+                }
                 debug!("{:?}", elst);
             }
             _ => skip_box_content(&mut b)?,
         };
         check_parser_state!(b.content);
     }
     Ok(())
 }
@@ -1063,19 +1069,16 @@ fn read_tkhd<T: Read>(src: &mut BMFFBox<
         matrix: matrix,
     })
 }
 
 /// Parse a elst box.
 fn read_elst<T: Read>(src: &mut BMFFBox<T>) -> Result<EditListBox> {
     let (version, _) = read_fullbox_extra(src)?;
     let edit_count = be_u32_with_limit(src)?;
-    if edit_count == 0 {
-        return Err(Error::InvalidData("invalid edit count"));
-    }
     let mut edits = Vec::new();
     for _ in 0..edit_count {
         let (segment_duration, media_time) = match version {
             1 => {
                 // 64 bit segment duration and media times.
                 (be_u64(src)?, be_i64(src)?)
             }
             0 => {
@@ -1089,16 +1092,19 @@ fn read_elst<T: Read>(src: &mut BMFFBox<
         vec_push(&mut edits, Edit {
             segment_duration: segment_duration,
             media_time: media_time,
             media_rate_integer: media_rate_integer,
             media_rate_fraction: media_rate_fraction,
         })?;
     }
 
+    // Padding could be added in some contents.
+    skip_box_remain(src)?;
+
     Ok(EditListBox {
         edits: edits,
     })
 }
 
 /// Parse a mdhd box.
 fn read_mdhd<T: Read>(src: &mut BMFFBox<T>) -> Result<MediaHeaderBox> {
     let (version, _) = read_fullbox_extra(src)?;
--- a/media/mp4parse-rust/mp4parse/src/tests.rs
+++ b/media/mp4parse-rust/mp4parse/src/tests.rs
@@ -754,19 +754,18 @@ fn read_elst_zero_entries() {
     let mut stream = make_fullbox(BoxSize::Auto, b"elst", 0, |s| {
         s.B32(0)
          .B16(12)
          .B16(34)
     });
     let mut iter = super::BoxIter::new(&mut stream);
     let mut stream = iter.next_box().unwrap().unwrap();
     match super::read_elst(&mut stream) {
-        Err(Error::InvalidData(s)) => assert_eq!(s, "invalid edit count"),
-        Ok(_) => panic!("expected an error result"),
-        _ => panic!("expected a different error result"),
+        Ok(elst) => assert_eq!(elst.edits.len(), 0),
+        _ => panic!("expected no error"),
     }
 }
 
 fn make_elst() -> Cursor<Vec<u8>> {
     make_fullbox(BoxSize::Auto, b"elst", 1, |s| {
         s.B32(1)
         // first entry
          .B64(1234) // duration
@@ -775,26 +774,29 @@ fn make_elst() -> Cursor<Vec<u8>> {
          .B16(34) // rate fraction
     })
 }
 
 #[test]
 fn read_edts_bogus() {
     // First edit list entry has a media_time of -1, so we expect a second
     // edit list entry to be present to provide a valid media_time.
+    // Bogus edts are ignored.
     let mut stream = make_box(BoxSize::Auto, b"edts", |s| {
         s.append_bytes(&make_elst().into_inner())
     });
     let mut iter = super::BoxIter::new(&mut stream);
     let mut stream = iter.next_box().unwrap().unwrap();
     let mut track = super::Track::new(0);
     match super::read_edts(&mut stream, &mut track) {
-        Err(Error::InvalidData(s)) => assert_eq!(s, "expected additional edit"),
-        Ok(_) => panic!("expected an error result"),
-        _ => panic!("expected a different error result"),
+        Ok(_) => {
+            assert_eq!(track.media_time, None);
+            assert_eq!(track.empty_duration, None);
+        }
+        _ => panic!("expected no error"),
     }
 }
 
 #[test]
 fn skip_padding_in_boxes() {
     // Padding data could be added in the end of these boxes. Parser needs to skip
     // them instead of returning error.
     let box_names = vec![b"stts", b"stsc", b"stsz", b"stco", b"co64", b"stss"];
--- a/media/mp4parse-rust/update-rust.sh
+++ b/media/mp4parse-rust/update-rust.sh
@@ -1,13 +1,13 @@
 #!/bin/sh -e
 # Script to update mp4parse-rust sources to latest upstream
 
 # Default version.
-VER="9e70cb418401c152cd3183aab06b084c0ce3f3e6"
+VER="2dc5127a69bc9bf891972e269e3abde0b77612f5"
 
 # Accept version or commit from the command line.
 if test -n "$1"; then
   VER=$1
 fi
 
 echo "Fetching sources..."
 rm -rf _upstream
deleted file mode 100644
--- a/testing/web-platform/meta/webdriver/tests/element_click/select.py.ini
+++ /dev/null
@@ -1,3 +0,0 @@
-[select.py]
-  disabled:
-    if debug: https://bugzilla.mozilla.org/show_bug.cgi?id=1397219
--- a/toolkit/components/extensions/test/browser/browser_ext_themes_toolbar_fields.js
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_toolbar_fields.js
@@ -31,18 +31,22 @@ add_task(async function test_support_too
     },
     files: {
       "image1.png": BACKGROUND,
     },
   });
 
   await extension.startup();
 
+  let root = document.documentElement;
   // Remove the `remotecontrol` attribute since it interferes with the urlbar styling.
-  document.documentElement.removeAttribute("remotecontrol");
+  root.removeAttribute("remotecontrol");
+  registerCleanupFunction(() => {
+    root.setAttribute("remotecontrol", "true");
+  });
 
   let toolbox = document.querySelector("#navigator-toolbox");
   let searchbar = document.querySelector("#searchbar");
   let fields = [
     toolbox.querySelector("#urlbar"),
     document.getAnonymousElementByAttribute(searchbar, "anonid", "searchbar-textbox"),
   ].filter(field => {
     let bounds = field.getBoundingClientRect();
@@ -58,13 +62,65 @@ add_task(async function test_support_too
                  hexToCSS(TOOLBAR_FIELD_BACKGROUND),
                  "Field background should be set.");
     Assert.equal(window.getComputedStyle(field).color,
                  hexToCSS(TOOLBAR_FIELD_COLOR),
                  "Field color should be set.");
     testBorderColor(field, TOOLBAR_FIELD_BORDER);
   }
 
-  // Restore the `remotecontrol` attribute at the end of the test.
-  document.documentElement.setAttribute("remotecontrol", "true");
+  await extension.unload();
+});
+
+add_task(async function test_support_toolbar_field_brighttext() {
+  let root = document.documentElement;
+  // Remove the `remotecontrol` attribute since it interferes with the urlbar styling.
+  root.removeAttribute("remotecontrol");
+  registerCleanupFunction(() => {
+    root.setAttribute("remotecontrol", "true");
+  });
+  let toolbox = document.querySelector("#navigator-toolbox");
+  let urlbar = toolbox.querySelector("#urlbar");
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "theme": {
+        "colors": {
+          "accentcolor": ACCENT_COLOR,
+          "textcolor": TEXT_COLOR,
+          "toolbar_field": "#fff",
+          "toolbar_field_text": "#000",
+        },
+      },
+    },
+  });
+
+  await extension.startup();
+
+  Assert.equal(window.getComputedStyle(urlbar).color,
+               hexToCSS("#000000"), "Color has been set");
+  Assert.ok(!root.hasAttribute("lwt-toolbar-field-brighttext"),
+            "Brighttext attribute should not be set");
+
+  await extension.unload();
+
+  extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "theme": {
+        "colors": {
+          "accentcolor": ACCENT_COLOR,
+          "textcolor": TEXT_COLOR,
+          "toolbar_field": "#000",
+          "toolbar_field_text": "#fff",
+        },
+      },
+    },
+  });
+
+  await extension.startup();
+
+  Assert.equal(window.getComputedStyle(urlbar).color,
+               hexToCSS("#ffffff"), "Color has been set");
+  Assert.ok(root.hasAttribute("lwt-toolbar-field-brighttext"),
+            "Brighttext attribute should be set");
 
   await extension.unload();
 });
--- a/toolkit/modules/LightweightThemeConsumer.jsm
+++ b/toolkit/modules/LightweightThemeConsumer.jsm
@@ -24,17 +24,17 @@ const toolkitVariableMap = [
     lwtProperty: "textcolor",
     processColor(rgbaChannels, element) {
       if (!rgbaChannels) {
         element.removeAttribute("lwthemetextcolor");
         element.removeAttribute("lwtheme");
         return null;
       }
       const {r, g, b, a} = rgbaChannels;
-      const luminance = 0.2125 * r + 0.7154 * g + 0.0721 * b;
+      const luminance = _getLuminance(r, g, b);
       element.setAttribute("lwthemetextcolor", luminance <= 110 ? "dark" : "bright");
       element.setAttribute("lwtheme", "true");
       return `rgba(${r}, ${g}, ${b}, ${a})` || "black";
     }
   }],
   ["--arrowpanel-background", {
     lwtProperty: "popup"
   }],
@@ -43,17 +43,31 @@ const toolkitVariableMap = [
   }],
   ["--arrowpanel-border-color", {
     lwtProperty: "popup_border"
   }],
   ["--lwt-toolbar-field-background-color", {
     lwtProperty: "toolbar_field"
   }],
   ["--lwt-toolbar-field-color", {
-    lwtProperty: "toolbar_field_text"
+    lwtProperty: "toolbar_field_text",
+    processColor(rgbaChannels, element) {
+      if (!rgbaChannels) {
+        element.removeAttribute("lwt-toolbar-field-brighttext");
+        return null;
+      }
+      const {r, g, b, a} = rgbaChannels;
+      const luminance = _getLuminance(r, g, b);
+      if (luminance <= 110) {
+        element.removeAttribute("lwt-toolbar-field-brighttext");
+      } else {
+        element.setAttribute("lwt-toolbar-field-brighttext", "true");
+      }
+      return `rgba(${r}, ${g}, ${b}, ${a})`;
+    }
   }],
 ];
 
 // Get the theme variables from the app resource directory.
 // This allows per-app variables.
 ChromeUtils.import("resource:///modules/ThemeVariableMap.jsm");
 
 ChromeUtils.defineModuleGetter(this, "LightweightThemeImageOptimizer",
@@ -238,8 +252,12 @@ function _parseRGBA(aColorString) {
   rgba = rgba.map(x => parseFloat(x));
   return {
     r: rgba[0],
     g: rgba[1],
     b: rgba[2],
     a: 3 in rgba ? rgba[3] : 1,
   };
 }
+
+function _getLuminance(r, g, b) {
+  return 0.2125 * r + 0.7154 * g + 0.0721 * b;
+}
--- a/widget/gtk/WidgetStyleCache.cpp
+++ b/widget/gtk/WidgetStyleCache.cpp
@@ -1288,16 +1288,24 @@ GetCssNodeStyleInternal(WidgetNodeType a
     case MOZ_GTK_WINDOW_DECORATION:
     {
       GtkStyleContext* parentStyle =
           CreateSubStyleWithClass(MOZ_GTK_WINDOW, "csd");
       style = CreateCSSNode("decoration", parentStyle);
       g_object_unref(parentStyle);
       break;
     }
+    case MOZ_GTK_WINDOW_DECORATION_SOLID:
+    {
+      GtkStyleContext* parentStyle =
+          CreateSubStyleWithClass(MOZ_GTK_WINDOW, "solid-csd");
+      style = CreateCSSNode("decoration", parentStyle);
+      g_object_unref(parentStyle);
+      break;
+    }
     default:
       return GetWidgetRootStyle(aNodeType);
   }
 
   MOZ_ASSERT(style, "missing style context for node type");
   sStyleStorage[aNodeType] = style;
   return style;
 }
--- a/widget/gtk/gtk3drawing.cpp
+++ b/widget/gtk/gtk3drawing.cpp
@@ -3115,19 +3115,23 @@ GetActiveScrollbarMetrics(GtkOrientation
   return metrics;
 }
 
 /*
  * get_shadow_width() from gtkwindow.c is not public so we need
  * to implement it.
  */
 bool
-GetCSDDecorationSize(GtkBorder* aDecorationSize)
+GetCSDDecorationSize(GtkWindow *aGtkWindow, GtkBorder* aDecorationSize)
 {
-    GtkStyleContext* context = GetStyleContext(MOZ_GTK_WINDOW_DECORATION);
+    GtkStyleContext* context = gtk_widget_get_style_context(GTK_WIDGET(aGtkWindow));
+    bool solidDecorations = gtk_style_context_has_class(context, "solid-csd");
+    context = GetStyleContext(solidDecorations ?
+                              MOZ_GTK_WINDOW_DECORATION_SOLID :
+                              MOZ_GTK_WINDOW_DECORATION);
 
     /* Always sum border + padding */
     GtkBorder padding;
     GtkStateFlags state = gtk_style_context_get_state(context);
     gtk_style_context_get_border(context, state, aDecorationSize);
     gtk_style_context_get_padding(context, state, &padding);
     *aDecorationSize += padding;
 
@@ -3146,20 +3150,24 @@ GetCSDDecorationSize(GtkBorder* aDecorat
         GdkRectangle clip;
         sGtkRenderBackgroundGetClip(context, 0, 0, 0, 0, &clip);
 
         extents.top = -clip.y;
         extents.right = clip.width + clip.x;
         extents.bottom = clip.height + clip.y;
         extents.left = -clip.x;
 
-        extents.top = MAX(extents.top, margin.top);
-        extents.right = MAX(extents.right, margin.right);
-        extents.bottom = MAX(extents.bottom, margin.bottom);
-        extents.left = MAX(extents.left, margin.left);
+        // Margin is used for resize grip size - it's not present on
+        // popup windows.
+        if (gtk_window_get_window_type(aGtkWindow) != GTK_WINDOW_POPUP) {
+            extents.top = MAX(extents.top, margin.top);
+            extents.right = MAX(extents.right, margin.right);
+            extents.bottom = MAX(extents.bottom, margin.bottom);
+            extents.left = MAX(extents.left, margin.left);
+        }
     } else {
         /* If we can't get shadow extents use decoration-resize-handle instead
          * as a workaround. This is inspired by update_border_windows()
          * from gtkwindow.c although this is not 100% accurate as we emulate
          * the extents here.
          */
         gint handle;
         gtk_widget_style_get(GetWidget(MOZ_GTK_WINDOW),
--- a/widget/gtk/gtkdrawing.h
+++ b/widget/gtk/gtkdrawing.h
@@ -334,16 +334,17 @@ typedef enum {
   /* MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE_RESTORE is a state of
    * MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE button and it's used as
    * an icon placeholder only.
    */
   MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE_RESTORE,
 
   /* Client-side window decoration node. Available on GTK 3.20+. */
   MOZ_GTK_WINDOW_DECORATION,
+  MOZ_GTK_WINDOW_DECORATION_SOLID,
 
   MOZ_GTK_WIDGET_NODE_COUNT
 } WidgetNodeType;
 
 /*** General library functions ***/
 /**
  * Initializes the drawing library.  You must call this function
  * prior to using any other functionality.
@@ -614,20 +615,21 @@ GetToolbarButtonMetrics(WidgetNodeType a
                         TOOLBAR_BUTTONS wide.
  *
  * returns:    Number of returned entries at aButtonLayout.
  */
 int
 GetGtkHeaderBarButtonLayout(WidgetNodeType* aButtonLayout, int aMaxButtonNums);
 
 /**
- * Get size of CSD window extents.
+ * Get size of CSD window extents of given GtkWindow.
  *
+ * aGtkWindow      [IN]  Decorated window.
  * aDecorationSize [OUT] Returns calculated (or estimated) decoration
- *                       size of CSD window.
+ *                       size of given aGtkWindow.
  *
  * returns:    True if we have extract decoration size (for GTK 3.20+)
  *             False if we have only an estimation (for GTK+ before  3.20+)
  */
 bool
-GetCSDDecorationSize(GtkBorder* aDecorationSize);
+GetCSDDecorationSize(GtkWindow *aGtkWindow, GtkBorder* aDecorationSize);
 
 #endif
--- a/widget/gtk/nsWindow.cpp
+++ b/widget/gtk/nsWindow.cpp
@@ -3322,16 +3322,43 @@ nsWindow::OnWindowStateEvent(GtkWidget *
               (GDK_WINDOW_STATE_ICONIFIED|GDK_WINDOW_STATE_WITHDRAWN));
         if (mHasMappedToplevel != mapped) {
             SetHasMappedToplevel(mapped);
         }
         return;
     }
     // else the widget is a shell widget.
 
+    // The block below is a bit evil.
+    //
+    // When a window is resized before it is shown, gtk_window_resize() delays
+    // resizes until the window is shown.  If gtk_window_state_event() sees a
+    // GDK_WINDOW_STATE_MAXIMIZED change [1] before the window is shown, then
+    // gtk_window_compute_configure_request_size() ignores the values from the
+    // resize [2].  See bug 1449166 for an example of how this could happen.
+    //
+    // [1] https://gitlab.gnome.org/GNOME/gtk/blob/3.22.30/gtk/gtkwindow.c#L7967
+    // [2] https://gitlab.gnome.org/GNOME/gtk/blob/3.22.30/gtk/gtkwindow.c#L9377
+    //
+    // In order to provide a sensible size for the window when the user exits
+    // maximized state, we hide the GDK_WINDOW_STATE_MAXIMIZED change from
+    // gtk_window_state_event() so as to trick GTK into using the values from
+    // gtk_window_resize() in its configure request.
+    //
+    // We instead notify gtk_window_state_event() of the maximized state change
+    // once the window is shown.
+    if (!mIsShown) {
+        aEvent->changed_mask = static_cast<GdkWindowState>
+            (aEvent->changed_mask & ~GDK_WINDOW_STATE_MAXIMIZED);
+    } else if (aEvent->changed_mask & GDK_WINDOW_STATE_WITHDRAWN &&
+               aEvent->new_window_state & GDK_WINDOW_STATE_MAXIMIZED) {
+        aEvent->changed_mask = static_cast<GdkWindowState>
+            (aEvent->changed_mask | GDK_WINDOW_STATE_MAXIMIZED);
+    }
+
     // We don't care about anything but changes in the maximized/icon/fullscreen
     // states
     if ((aEvent->changed_mask
          & (GDK_WINDOW_STATE_ICONIFIED |
             GDK_WINDOW_STATE_MAXIMIZED |
             GDK_WINDOW_STATE_FULLSCREEN)) == 0) {
         return;
     }
@@ -6603,17 +6630,17 @@ nsWindow::ClearCachedResources()
  */
 void
 nsWindow::UpdateClientOffsetForCSDWindow()
 {
     // _NET_FRAME_EXTENTS is not set on client decorated windows,
     // so we need to read offset between mContainer and toplevel mShell
     // window.
     GtkBorder decorationSize;
-    GetCSDDecorationSize(&decorationSize);
+    GetCSDDecorationSize(GTK_WINDOW(mShell), &decorationSize);
     mClientOffset = nsIntPoint(decorationSize.left, decorationSize.top);
 
     // Send a WindowMoved notification. This ensures that TabParent
     // picks up the new client offset and sends it to the child process
     // if appropriate.
     NotifyWindowMoved(mBounds.x, mBounds.y);
 }