merge autoland to mozilla-central. r=merge a=merge
authorSebastian Hengst <archaeopteryx@coole-files.de>
Sun, 20 Aug 2017 23:21:44 +0200
changeset 649612 0286df0f0eba0e81e2c628c332f90457c38ea926
parent 649600 cab35526779367774b3ecea2f7a8a5c0993caa77 (current diff)
parent 649611 daa30ecfd1d4c50e864fcacdde28d67eb260de24 (diff)
child 649622 7dddbd85047c6dc73ddbe1e423cd643a217845b3
push id75073
push userbmo:mozilla@hocat.ca
push dateSun, 20 Aug 2017 22:21:51 +0000
reviewersmerge, merge
milestone57.0a1
merge autoland to mozilla-central. r=merge a=merge MozReview-Commit-ID: o58z45INT1
--- a/.clang-format-ignore
+++ b/.clang-format-ignore
@@ -1,31 +1,50 @@
-^build/clang-plugin/tests/.*
+^build/clang-plugin/.*
 ^config/gcc-stl-wrapper.template.h
 ^config/msvc-stl-wrapper.template.h
+^dom/base/test/.*
+^dom/bindings/test/.*
+^dom/media/gtest/.*
+^gfx/testsd/.*
+^image/test/.*
+^ipc/ipdl/test/.*
+^ipc/testshell/.*
 ^js/src/jsapi-tests/.*
+^layout/style/nsCSSPropAliasList.h
+^layout/style/nsCSSPropList.h
+^media/mtransport/test/.*
+^mfbt/tests/.*
+^storage/test/.*
+^testing/gtest/.*
+^tools/profiler/tests/.*
+^uriloader/exthandler/tests/.*
 ^widget/android/GeneratedJNINatives.h
 ^widget/android/GeneratedJNIWrappers.cpp
 ^widget/android/GeneratedJNIWrappers.h
 ^widget/android/fennec/FennecJNINatives.h
 ^widget/android/fennec/FennecJNIWrappers.cpp
 ^widget/android/fennec/FennecJNIWrappers.h
+^widget/tests/.*
+^xpcom/glue/tests/.*
+^xpcom/tests/.*
 
 # Generated from ./tools/rewriting/ThirdPartyPaths.txt
 # awk '{print "^"$1".*"}' ./tools/rewriting/ThirdPartyPaths.txt
 ^browser/components/translation/cld2/.*
 ^db/sqlite3/src/.*
 ^dom/media/platforms/ffmpeg/libav.*
 ^extensions/spellcheck/hunspell/src/.*
 ^gfx/angle/.*
 ^gfx/cairo/.*
 ^gfx/graphite2/.*
 ^gfx/harfbuzz/.*
 ^gfx/ots/.*
 ^gfx/qcms/.*
+^gfx/sfntly/.*
 ^gfx/skia/.*
 ^gfx/vr/openvr/.*
 ^gfx/webrender.*
 ^gfx/webrender_api.*
 ^gfx/ycbcr/.*
 ^intl/hyphenation/hyphen/.*
 ^intl/icu/.*
 ^ipc/chromium/.*
@@ -62,28 +81,32 @@
 ^mfbt/lz4.*
 ^mobile/android/geckoview/src/thirdparty/.*
 ^mobile/android/thirdparty/.*
 ^modules/brotli/.*
 ^modules/fdlibm/.*
 ^modules/freetype2/.*
 ^modules/libbz2/.*
 ^modules/libmar/.*
+^modules/pdfium/.*
 ^modules/woff2/.*
 ^modules/zlib/.*
 ^netwerk/sctp/src/.*
 ^netwerk/srtp/src/.*
 ^nsprpub/.*
 ^other-licenses/.*
 ^parser/expat/.*
 ^security/nss/.*
 ^security/sandbox/chromium/.*
 ^testing/gtest/gmock/.*
 ^testing/gtest/gtest/.*
+^testing/talos/talos/tests/canvasmark/.*
 ^testing/talos/talos/tests/dromaeo/.*
+^testing/talos/talos/tests/kraken/.*
+^testing/talos/talos/tests/v8_7/.*
 ^third_party/aom/.*
 ^third_party/python/blessings/.*
 ^third_party/python/configobj/.*
 ^third_party/python/futures/.*
 ^third_party/python/jsmin/.*
 ^third_party/python/mock-*/.*
 ^third_party/python/psutil/.*
 ^third_party/python/py/.*
@@ -92,11 +115,13 @@
 ^third_party/python/PyECC/.*
 ^third_party/python/pytest/.*
 ^third_party/python/pytoml/.*
 ^third_party/python/pyyaml/.*
 ^third_party/python/redo/.*
 ^third_party/python/requests/.*
 ^third_party/python/rsa/.*
 ^third_party/python/which/.*
+^third_party/rust/.*
 ^toolkit/components/jsoncpp/.*
 ^toolkit/components/protobuf/.*
 ^toolkit/crashreporter/google-breakpad/.*
+^tools/fuzzing/libfuzzer.*
--- a/devtools/client/inspector/markup/test/head.js
+++ b/devtools/client/inspector/markup/test/head.js
@@ -7,17 +7,17 @@
 "use strict";
 
 // Import the inspector's head.js first (which itself imports shared-head.js).
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
   this);
 
 var {getInplaceEditorForSpan: inplaceEditor} = require("devtools/client/shared/inplace-editor");
-var clipboard = require("sdk/clipboard");
+var clipboard = require("devtools/shared/platform/clipboard");
 var {ActorRegistryFront} = require("devtools/shared/fronts/actor-registry");
 
 // If a test times out we want to see the complete log and not just the last few
 // lines.
 SimpleTest.requestCompleteLog();
 
 // Set the testing flag on DevToolsUtils and reset it when the test ends
 flags.testing = true;
@@ -292,17 +292,17 @@ function searchUsingSelectorSearch(selec
  * @param {InspectorPanel} inspector
  * @param {Boolean} assert Should this function run assertions inline.
  * @return A promise that resolves with a boolean indicating whether
  *         the menu items are disabled once the menu has been checked.
  */
 var isEditingMenuDisabled = Task.async(
 function* (nodeFront, inspector, assert = true) {
   // To ensure clipboard contains something to paste.
-  clipboard.set("<p>test</p>", "html");
+  clipboard.copyString("<p>test</p>");
 
   yield selectNode(nodeFront, inspector);
   let allMenuItems = openContextMenuAndGetAllItems(inspector);
 
   let deleteMenuItem = allMenuItems.find(i => i.id === "node-menu-delete");
   let editHTMLMenuItem = allMenuItems.find(i => i.id === "node-menu-edithtml");
   let pasteHTMLMenuItem = allMenuItems.find(i => i.id === "node-menu-pasteouterhtml");
 
@@ -324,17 +324,17 @@ function* (nodeFront, inspector, assert 
  * @param {InspectorPanel} inspector
  * @param {Boolean} assert Should this function run assertions inline.
  * @return A promise that resolves with a boolean indicating whether
  *         the menu items are enabled once the menu has been checked.
  */
 var isEditingMenuEnabled = Task.async(
 function* (nodeFront, inspector, assert = true) {
   // To ensure clipboard contains something to paste.
-  clipboard.set("<p>test</p>", "html");
+  clipboard.copyString("<p>test</p>");
 
   yield selectNode(nodeFront, inspector);
   let allMenuItems = openContextMenuAndGetAllItems(inspector);
 
   let deleteMenuItem = allMenuItems.find(i => i.id === "node-menu-delete");
   let editHTMLMenuItem = allMenuItems.find(i => i.id === "node-menu-edithtml");
   let pasteHTMLMenuItem = allMenuItems.find(i => i.id === "node-menu-pasteouterhtml");
 
--- a/devtools/client/inspector/test/browser_inspector_menu-01-sensitivity.js
+++ b/devtools/client/inspector/test/browser_inspector_menu-01-sensitivity.js
@@ -57,77 +57,77 @@ const TEST_CASES = [
   {
     desc: "doctype node with empty clipboard",
     selector: null,
     disabled: INACTIVE_ON_DOCTYPE_ITEMS,
   },
   {
     desc: "doctype node with html on clipboard",
     clipboardData: "<p>some text</p>",
-    clipboardDataType: "html",
+    clipboardDataType: "text",
     selector: null,
     disabled: INACTIVE_ON_DOCTYPE_ITEMS,
   },
   {
     desc: "element node HTML on the clipboard",
     clipboardData: "<p>some text</p>",
-    clipboardDataType: "html",
+    clipboardDataType: "text",
     disabled: [
       "node-menu-copyimagedatauri",
       "node-menu-copy-attribute",
       "node-menu-edit-attribute",
       "node-menu-remove-attribute"
     ],
     selector: "#sensitivity",
   },
   {
     desc: "<html> element",
     clipboardData: "<p>some text</p>",
-    clipboardDataType: "html",
+    clipboardDataType: "text",
     selector: "html",
     disabled: [
       "node-menu-copyimagedatauri",
       "node-menu-pastebefore",
       "node-menu-pasteafter",
       "node-menu-pastefirstchild",
       "node-menu-pastelastchild",
       "node-menu-copy-attribute",
       "node-menu-edit-attribute",
       "node-menu-remove-attribute"
     ],
   },
   {
     desc: "<body> with HTML on clipboard",
     clipboardData: "<p>some text</p>",
-    clipboardDataType: "html",
+    clipboardDataType: "text",
     selector: "body",
     disabled: [
       "node-menu-copyimagedatauri",
       "node-menu-pastebefore",
       "node-menu-pasteafter",
       "node-menu-copy-attribute",
       "node-menu-edit-attribute",
       "node-menu-remove-attribute"
     ]
   },
   {
     desc: "<img> with HTML on clipboard",
     clipboardData: "<p>some text</p>",
-    clipboardDataType: "html",
+    clipboardDataType: "text",
     selector: "img",
     disabled: [
       "node-menu-copy-attribute",
       "node-menu-edit-attribute",
       "node-menu-remove-attribute"
     ]
   },
   {
     desc: "<head> with HTML on clipboard",
     clipboardData: "<p>some text</p>",
-    clipboardDataType: "html",
+    clipboardDataType: "text",
     selector: "head",
     disabled: [
       "node-menu-copyimagedatauri",
       "node-menu-pastebefore",
       "node-menu-pasteafter",
       "node-menu-screenshotnode",
       "node-menu-copy-attribute",
       "node-menu-edit-attribute",
@@ -143,55 +143,55 @@ const TEST_CASES = [
       "node-menu-copy-attribute",
       "node-menu-edit-attribute",
       "node-menu-remove-attribute"
     ]),
   },
   {
     desc: "<element> with text on clipboard",
     clipboardData: "some text",
-    clipboardDataType: undefined,
+    clipboardDataType: "text",
     selector: "#paste-area",
     disabled: [
       "node-menu-copyimagedatauri",
       "node-menu-copy-attribute",
       "node-menu-edit-attribute",
       "node-menu-remove-attribute"
     ]
   },
   {
     desc: "<element> with base64 encoded image data uri on clipboard",
     clipboardData:
-      "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABC" +
+      "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABC" +
       "AAAAAA6fptVAAAACklEQVQYV2P4DwABAQEAWk1v8QAAAABJRU5ErkJggg==",
-    clipboardDataType: undefined,
+    clipboardDataType: "image",
     selector: "#paste-area",
     disabled: PASTE_MENU_ITEMS.concat([
       "node-menu-copyimagedatauri",
       "node-menu-copy-attribute",
       "node-menu-edit-attribute",
       "node-menu-remove-attribute"
     ]),
   },
   {
     desc: "<element> with empty string on clipboard",
     clipboardData: "",
-    clipboardDataType: undefined,
+    clipboardDataType: "text",
     selector: "#paste-area",
     disabled: PASTE_MENU_ITEMS.concat([
       "node-menu-copyimagedatauri",
       "node-menu-copy-attribute",
       "node-menu-edit-attribute",
       "node-menu-remove-attribute"
     ]),
   },
   {
     desc: "<element> with whitespace only on clipboard",
     clipboardData: " \n\n\t\n\n  \n",
-    clipboardDataType: undefined,
+    clipboardDataType: "text",
     selector: "#paste-area",
     disabled: PASTE_MENU_ITEMS.concat([
       "node-menu-copyimagedatauri",
       "node-menu-copy-attribute",
       "node-menu-edit-attribute",
       "node-menu-remove-attribute"
     ]),
   },
@@ -220,18 +220,19 @@ const TEST_CASES = [
   {
     desc: "<element> with context menu triggered on attribute, empty clipboard",
     selector: "#attributes",
     disabled: PASTE_MENU_ITEMS.concat(["node-menu-copyimagedatauri"]),
     attributeTrigger: "data-edit"
   }
 ];
 
-var clipboard = require("sdk/clipboard");
+var clipboard = require("devtools/shared/platform/clipboard");
 registerCleanupFunction(() => {
+  clipboard.copyString("");
   clipboard = null;
 });
 
 add_task(function* () {
   let { inspector } = yield openInspectorForURL(TEST_URL);
   for (let test of TEST_CASES) {
     let { desc, disabled, selector, attributeTrigger } = test;
 
@@ -253,17 +254,17 @@ add_task(function* () {
     let allMenuItems = openContextMenuAndGetAllItems(inspector, {
       target: contextMenuTrigger,
     });
 
     for (let id of ALL_MENU_ITEMS) {
       let menuItem = allMenuItems.find(item => item.id === id);
       let shouldBeDisabled = disabled.indexOf(id) !== -1;
       is(menuItem.disabled, shouldBeDisabled,
-        `#${id} should be ${shouldBeDisabled ? "disabled" : "enabled"} `);
+        `#${id} should be ${shouldBeDisabled ? "disabled" : "enabled"}`);
     }
   }
 });
 
 /**
  * A helper that fetches a front for a node that matches the given selector or
  * doctype node if the selector is falsy.
  */
@@ -278,16 +279,48 @@ function* getNodeFrontForSelector(select
   return nodes[0];
 }
 
 /**
  * A helper that populates the clipboard with data of given type. Clears the
  * clipboard if data is falsy.
  */
 function setupClipboard(data, type) {
-  if (data) {
-    info("Populating clipboard with " + type + " data.");
-    clipboard.set(data, type);
-  } else {
-    info("Clearing clipboard.");
-    clipboard.set("", "text");
+  if (!data) {
+    info("Clearing the clipboard.");
+    clipboard.copyString("");
+  } else if (type === "text") {
+    info("Populating clipboard with text.");
+    clipboard.copyString(data);
+  } else if (type === "image") {
+    info("Populating clipboard with image content");
+    copyImageToClipboard(data);
   }
 }
+
+/**
+ * The code below is a simplified version of the sdk/clipboard helper set() method.
+ */
+function copyImageToClipboard(data) {
+  let clipboardService = Cc["@mozilla.org/widget/clipboard;1"]
+                              .getService(Ci.nsIClipboard);
+  let imageTools = Cc["@mozilla.org/image/tools;1"]
+                     .getService(Ci.imgITools);
+
+  // Image data is stored as base64 in the test.
+  let image = atob(data);
+
+  let input = Cc["@mozilla.org/io/string-input-stream;1"]
+                .createInstance(Ci.nsIStringInputStream);
+  input.setData(image, image.length);
+
+  let imgPtr = Cc["@mozilla.org/supports-interface-pointer;1"]
+                 .createInstance(Ci.nsISupportsInterfacePointer);
+  imgPtr.data = imageTools.decodeImage(input, "image/png");
+
+  let xferable = Cc["@mozilla.org/widget/transferable;1"]
+                   .createInstance(Ci.nsITransferable);
+  xferable.init(null);
+  xferable.addDataFlavor("image/png");
+  xferable.setTransferData("image/png", imgPtr, -1);
+
+  clipboardService.setData(xferable, null, clipboardService.kGlobalClipboard);
+}
--- a/devtools/client/inspector/test/browser_inspector_menu-03-paste-items-svg.js
+++ b/devtools/client/inspector/test/browser_inspector_menu-03-paste-items-svg.js
@@ -5,17 +5,17 @@ http://creativecommons.org/publicdomain/
 
 // Test that HTML can be pasted in SVG elements.
 
 const TEST_URL = URL_ROOT + "doc_inspector_svg.svg";
 const PASTE_AS_FIRST_CHILD = '<circle xmlns="http://www.w3.org/2000/svg" cx="42" cy="42" r="5"/>';
 const PASTE_AS_LAST_CHILD = '<circle xmlns="http://www.w3.org/2000/svg" cx="42" cy="42" r="15"/>';
 
 add_task(function* () {
-  let clipboard = require("sdk/clipboard");
+  let clipboard = require("devtools/shared/platform/clipboard");
 
   let { inspector, testActor } = yield openInspectorForURL(TEST_URL);
 
   let refSelector = "svg";
   let oldHTML = yield testActor.getProperty(refSelector, "innerHTML");
   yield selectNode(refSelector, inspector);
   let markupTagLine = getContainerForSelector(refSelector, inspector).tagLine;
 
@@ -27,16 +27,16 @@ add_task(function* () {
   is(html, expectedHtml, "The innerHTML of the SVG node is correct");
 
   // Helpers
   function* pasteContent(menuId, clipboardData) {
     let allMenuItems = openContextMenuAndGetAllItems(inspector, {
       target: markupTagLine,
     });
     info(`Testing ${menuId} for ${clipboardData}`);
-    clipboard.set(clipboardData);
+    clipboard.copyString(clipboardData);
 
     let onMutation = inspector.once("markupmutation");
     allMenuItems.find(item => item.id === menuId).click();
     info("Waiting for mutation to occur");
     yield onMutation;
   }
 });
--- a/devtools/client/inspector/test/browser_inspector_menu-03-paste-items.js
+++ b/devtools/client/inspector/test/browser_inspector_menu-03-paste-items.js
@@ -24,56 +24,56 @@ const PASTE_ADJACENT_HTML_DATA = [
   },
   {
     desc: "After",
     clipboardData: "<span>5</span>",
     menuId: "node-menu-pasteafter",
   },
 ];
 
-var clipboard = require("sdk/clipboard");
+var clipboard = require("devtools/shared/platform/clipboard");
 registerCleanupFunction(() => {
   clipboard = null;
 });
 
 add_task(function* () {
   let { inspector, testActor } = yield openInspectorForURL(TEST_URL);
 
   yield testPasteOuterHTMLMenu();
   yield testPasteInnerHTMLMenu();
   yield testPasteAdjacentHTMLMenu();
 
   function* testPasteOuterHTMLMenu() {
     info("Testing that 'Paste Outer HTML' menu item works.");
-    clipboard.set("this was pasted (outerHTML)");
+    clipboard.copyString("this was pasted (outerHTML)");
     let outerHTMLSelector = "#paste-area h1";
 
     let nodeFront = yield getNodeFront(outerHTMLSelector, inspector);
     yield selectNode(nodeFront, inspector);
 
     let allMenuItems = openContextMenuAndGetAllItems(inspector, {
       target: getContainerForNodeFront(nodeFront, inspector).tagLine,
     });
 
     let onNodeReselected = inspector.markup.once("reselectedonremoved");
     allMenuItems.find(item => item.id === "node-menu-pasteouterhtml").click();
 
     info("Waiting for inspector selection to update");
     yield onNodeReselected;
 
     let outerHTML = yield testActor.getProperty("body", "outerHTML");
-    ok(outerHTML.includes(clipboard.get()),
+    ok(outerHTML.includes(clipboard.getText()),
        "Clipboard content was pasted into the node's outer HTML.");
     ok(!(yield testActor.hasNode(outerHTMLSelector)),
       "The original node was removed.");
   }
 
   function* testPasteInnerHTMLMenu() {
     info("Testing that 'Paste Inner HTML' menu item works.");
-    clipboard.set("this was pasted (innerHTML)");
+    clipboard.copyString("this was pasted (innerHTML)");
     let innerHTMLSelector = "#paste-area .inner";
     let getInnerHTML = () => testActor.getProperty(innerHTMLSelector,
                                                    "innerHTML");
     let origInnerHTML = yield getInnerHTML();
 
     let nodeFront = yield getNodeFront(innerHTMLSelector, inspector);
     yield selectNode(nodeFront, inspector);
 
@@ -81,17 +81,17 @@ add_task(function* () {
       target: getContainerForNodeFront(nodeFront, inspector).tagLine,
     });
 
     let onMutation = inspector.once("markupmutation");
     allMenuItems.find(item => item.id === "node-menu-pasteinnerhtml").click();
     info("Waiting for mutation to occur");
     yield onMutation;
 
-    ok((yield getInnerHTML()) === clipboard.get(),
+    ok((yield getInnerHTML()) === clipboard.getText(),
        "Clipboard content was pasted into the node's inner HTML.");
     ok((yield testActor.hasNode(innerHTMLSelector)),
        "The original node has been preserved.");
     yield undoChange(inspector);
     ok((yield getInnerHTML()) === origInnerHTML,
        "Previous innerHTML has been restored after undo");
   }
 
@@ -102,17 +102,17 @@ add_task(function* () {
     yield selectNode(nodeFront, inspector);
     let markupTagLine = getContainerForNodeFront(nodeFront, inspector).tagLine;
 
     for (let { clipboardData, menuId } of PASTE_ADJACENT_HTML_DATA) {
       let allMenuItems = openContextMenuAndGetAllItems(inspector, {
         target: markupTagLine,
       });
       info(`Testing ${menuId} for ${clipboardData}`);
-      clipboard.set(clipboardData);
+      clipboard.copyString(clipboardData);
 
       let onMutation = inspector.once("markupmutation");
       allMenuItems.find(item => item.id === menuId).click();
       info("Waiting for mutation to occur");
       yield onMutation;
     }
 
     let html = yield testActor.getProperty(adjacentNodeSelector, "innerHTML");
--- a/dom/animation/test/mochitest.ini
+++ b/dom/animation/test/mochitest.ini
@@ -118,8 +118,9 @@ skip-if = toolkit == 'android'
 [mozilla/test_underlying-discrete-value.html]
 [style/test_animation-seeking-with-current-time.html]
 [style/test_animation-seeking-with-start-time.html]
 [style/test_animation-setting-effect.html]
 [style/test_composite.html]
 [style/test_interpolation-from-interpolatematrix-to-none.html]
 [style/test_missing-keyframe.html]
 [style/test_missing-keyframe-on-compositor.html]
+[style/test_transform-non-normalizable-rotate3d.html]
new file mode 100644
--- /dev/null
+++ b/dom/animation/test/style/test_transform-non-normalizable-rotate3d.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script src='../testcommon.js'></script>
+<div id='log'></div>
+<script type='text/javascript'>
+'use strict';
+
+test(function(t) {
+  var target = addDiv(t);
+  target.style.transform = 'rotate3d(0, 0, 1, 90deg)';
+  target.style.transition = 'all 10s linear -5s';
+  getComputedStyle(target).transform;
+
+  target.style.transform = 'rotate3d(0, 0, 0, 270deg)';
+  var interpolated_matrix = 'matrix(' + Math.cos(Math.PI / 4) + ',' +
+                                        Math.sin(Math.PI / 4) + ',' +
+                                       -Math.sin(Math.PI / 4) + ',' +
+                                        Math.cos(Math.PI / 4) + ',' +
+                                        '0, 0)';
+  assert_matrix_equals(getComputedStyle(target).transform, interpolated_matrix,
+                       'transition from a normal rotate3d to a ' +
+                       'non-normalizable rotate3d');
+}, 'Test interpolation on non-normalizable rotate3d function');
+
+</script>
+</html>
--- a/dom/canvas/moz.build
+++ b/dom/canvas/moz.build
@@ -183,17 +183,17 @@ UNIFIED_SOURCES += [
     'WebGLVertexAttribData.cpp',
 ]
 
 SOURCES += [
     'MurmurHash3.cpp',
 ]
 
 # Suppress warnings from third-party code.
-if CONFIG['CLANG_CXX']:
+if CONFIG['CLANG_CXX'] or CONFIG['GNU_CXX']:
     SOURCES['MurmurHash3.cpp'].flags += ['-Wno-implicit-fallthrough']
 
 LOCAL_INCLUDES += [
     '/js/xpconnect/wrappers',
 ]
 
 include('/ipc/chromium/chromium-config.mozbuild')
 
--- a/gfx/ots/README.mozilla
+++ b/gfx/ots/README.mozilla
@@ -1,12 +1,12 @@
 This is the Sanitiser for OpenType project, from http://code.google.com/p/ots/.
 
 Our reference repository is https://github.com/khaledhosny/ots/.
 
-Current revision: 57ef618b11aa0409637af04988ccce7e6b92ed0f (5.2.0)
+Current revision: e2d4b5daba24e746a48d240e90d92fe09a20b681 (5.2.0)
 
 Upstream files included: LICENSE, src/, include/, tests/*.cc
 
 Additional files: README.mozilla, src/moz.build
 
 Additional patch: ots-visibility.patch (bug 711079).
 Additional patch: ots-lz4.patch
--- a/gfx/ots/src/glat.cc
+++ b/gfx/ots/src/glat.cc
@@ -69,17 +69,17 @@ bool OpenTypeGLAT_v1::GlatEntry::ParsePa
   if (!table.ReadU8(&this->attNum)) {
     return parent->Error("GlatEntry: Failed to read attNum");
   }
   if (!table.ReadU8(&this->num)) {
     return parent->Error("GlatEntry: Failed to read num");
   }
 
   //this->attributes.resize(this->num);
-  for (unsigned i = 0; i < this->num; ++i) {
+  for (int i = 0; i < this->num; ++i) {
     this->attributes.emplace_back();
     if (!table.ReadS16(&this->attributes[i])) {
       return parent->Error("GlatEntry: Failed to read attribute %u", i);
     }
   }
   return true;
 }
 
@@ -151,17 +151,17 @@ bool OpenTypeGLAT_v2::GlatEntry::ParsePa
   if (!table.ReadS16(&this->attNum)) {
     return parent->Error("GlatEntry: Failed to read attNum");
   }
   if (!table.ReadS16(&this->num) || this->num < 0) {
     return parent->Error("GlatEntry: Failed to read valid num");
   }
 
   //this->attributes.resize(this->num);
-  for (unsigned i = 0; i < this->num; ++i) {
+  for (int i = 0; i < this->num; ++i) {
     this->attributes.emplace_back();
     if (!table.ReadS16(&this->attributes[i])) {
       return parent->Error("GlatEntry: Failed to read attribute %u", i);
     }
   }
   return true;
 }
 
new file mode 100644
--- /dev/null
+++ b/layout/style/crashtests/1391577.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<style>
+.foo input[type="range"]::-moz-range-thumb:hover {
+  color: red;
+}
+</style>
+<div>
+  <input type="range"></input>
+</div>
+<script>
+onload = function() {
+  document.querySelector('div').classList.add('foo');
+}
+</script>
--- a/layout/style/crashtests/crashtests.list
+++ b/layout/style/crashtests/crashtests.list
@@ -196,8 +196,9 @@ load link-transition-before.html
 load long-url-list-stack-overflow.html
 load 1383981.html
 load 1383981-2.html
 load 1384824-1.html
 load 1384824-2.html
 load 1387481-1.html
 load 1387499.html
 load 1388234.html
+load 1391577.html
--- a/layout/style/test/test_transitions_per_property.html
+++ b/layout/style/test/test_transitions_per_property.html
@@ -376,25 +376,23 @@ var transformTests = [
     expected_uncomputed: 'rotate(360deg)',
     expected: computeMatrix('scale(1)'),
     round_error_ok: true },
   { start: 'none', end: 'rotatey(60deg)',
     expected_uncomputed: 'rotatey(15deg)',
     expected: computeMatrix('rotatey(15deg)') },
   { start: 'none', end: 'rotatey(720deg)',
     expected_uncomputed: 'rotatey(180deg)',
-    expected: computeMatrix('rotatey(180deg)'),
-    skipped: isServo },
+    expected: computeMatrix('rotatey(180deg)') },
   { start: 'none', end: 'rotatex(60deg)',
     expected_uncomputed: 'rotatex(15deg)',
     expected: computeMatrix('rotatex(15deg)') },
   { start: 'none', end: 'rotatex(720deg)',
     expected_uncomputed: 'rotatex(180deg)',
-    expected: computeMatrix('rotatex(180deg)'),
-    skipped: isServo },
+    expected: computeMatrix('rotatex(180deg)') },
 
   // translate
   { start: 'translate(20px)', end: 'none',
     expected_uncomputed: 'translate(15px)',
     expected: 'matrix(1, 0, 0, 1, 15, 0)' },
   { start: 'translate(20px, 12px)', end: 'none',
     expected_uncomputed: 'translate(15px, 9px)',
     expected: 'matrix(1, 0, 0, 1, 15, 9)' },
--- a/parser/html/moz.build
+++ b/parser/html/moz.build
@@ -94,12 +94,11 @@ UNIFIED_SOURCES += [
 FINAL_LIBRARY = 'xul'
 
 # DEFINES['ENABLE_VOID_MENUITEM'] = True
 
 LOCAL_INCLUDES += [
     '/dom/base',
 ]
 
-if CONFIG['GNU_CXX']:
-    CXXFLAGS += ['-Wno-error=shadow']
-    if CONFIG['CLANG_CXX']:
-        CXXFLAGS += ['-Wno-implicit-fallthrough']
+if CONFIG['GNU_CXX'] or CONFIG['CLANG_CXX']:
+    CXXFLAGS += ['-Wno-error=shadow',
+                 '-Wno-implicit-fallthrough']
--- a/security/sandbox/linux/moz.build
+++ b/security/sandbox/linux/moz.build
@@ -77,17 +77,17 @@ if CONFIG['MOZ_GMP_SANDBOX']:
         'SandboxOpenedFiles.cpp',
     ]
 
 # This copy of SafeSPrintf doesn't need to avoid the Chromium logging
 # dependency like the one in libxul does, but this way the behavior is
 # consistent.  See also the comment in SandboxLogging.h.
 SOURCES['../chromium/base/strings/safe_sprintf.cc'].flags += ['-DNDEBUG']
 
-if CONFIG['CLANG_CXX']:
+if CONFIG['CLANG_CXX'] or CONFIG['GNU_CXX']:
     # Keep clang from warning about intentional 'switch' fallthrough in icu_utf.cc:
     SOURCES['../chromium/base/third_party/icu/icu_utf.cc'].flags += ['-Wno-implicit-fallthrough']
     SOURCES['../chromium/sandbox/linux/seccomp-bpf/trap.cc'].flags += ['-Wno-unreachable-code-return']
 
 if CONFIG['GNU_CXX']:
     CXXFLAGS += ['-Wno-shadow']
     SOURCES['../chromium/sandbox/linux/services/syscall_wrappers.cc'].flags += [
         '-Wno-empty-body',
--- a/servo/components/layout/animation.rs
+++ b/servo/components/layout/animation.rs
@@ -156,19 +156,20 @@ pub fn recalc_style_for_animations(conte
         if let Some(ref animations) = animations.get(&fragment.node) {
             for animation in animations.iter() {
                 let old_style = fragment.style.clone();
                 update_style_for_animation(&context.style_context,
                                            animation,
                                            &mut fragment.style,
                                            &ServoMetricsProvider);
                 let difference =
-                    RestyleDamage::compute_style_difference(&old_style,
-                                                            &old_style,
-                                                            &fragment.style);
+                    RestyleDamage::compute_style_difference(
+                        &old_style,
+                        &fragment.style,
+                    );
                 damage |= difference.damage;
             }
         }
     });
 
     let base = flow::mut_base(flow);
     base.restyle_damage.insert(damage);
     for kid in base.children.iter_mut() {
--- a/servo/components/layout_thread/dom_wrapper.rs
+++ b/servo/components/layout_thread/dom_wrapper.rs
@@ -439,24 +439,16 @@ impl<'le> TElement for ServoLayoutElemen
             if let Some(ref classes) = self.element.get_classes_for_layout() {
                 for class in *classes {
                     callback(class)
                 }
             }
         }
     }
 
-    #[inline]
-    fn existing_style_for_restyle_damage<'a>(&'a self,
-                                             current_cv: &'a ComputedValues,
-                                             _pseudo_element: Option<&PseudoElement>)
-                                             -> Option<&'a ComputedValues> {
-        Some(current_cv)
-    }
-
     fn has_dirty_descendants(&self) -> bool {
         unsafe { self.as_node().node.get_flag(HAS_DIRTY_DESCENDANTS) }
     }
 
     fn has_snapshot(&self) -> bool {
         unsafe { self.as_node().node.get_flag(HAS_SNAPSHOT) }
     }
 
--- a/servo/components/style/dom.rs
+++ b/servo/components/style/dom.rs
@@ -15,17 +15,17 @@ use atomic_refcell::{AtomicRef, AtomicRe
 use data::ElementData;
 use element_state::ElementState;
 use font_metrics::FontMetricsProvider;
 use media_queries::Device;
 use properties::{AnimationRules, ComputedValues, PropertyDeclarationBlock};
 #[cfg(feature = "gecko")] use properties::animated_properties::AnimationValue;
 #[cfg(feature = "gecko")] use properties::animated_properties::TransitionProperty;
 use rule_tree::CascadeLevel;
-use selector_parser::{AttrValue, ElementExt, PreExistingComputedValues};
+use selector_parser::{AttrValue, ElementExt};
 use selector_parser::{PseudoClassStringArg, PseudoElement};
 use selectors::matching::{ElementSelectorFlags, VisitedHandlingMode};
 use selectors::sink::Push;
 use servo_arc::{Arc, ArcBorrow};
 use shared_lock::Locked;
 use smallvec::VecLike;
 use std::fmt;
 #[cfg(feature = "gecko")] use std::collections::HashMap;
@@ -373,28 +373,16 @@ pub trait TElement : Eq + PartialEq + De
     fn has_attr(&self, namespace: &Namespace, attr: &LocalName) -> bool;
 
     /// The ID for this element.
     fn get_id(&self) -> Option<Atom>;
 
     /// Internal iterator for the classes of this element.
     fn each_class<F>(&self, callback: F) where F: FnMut(&Atom);
 
-    /// Get the pre-existing style to calculate restyle damage (change hints).
-    ///
-    /// This needs to be generic since it varies between Servo and Gecko.
-    ///
-    /// XXX(emilio): It's a bit unfortunate we need to pass the current computed
-    /// values as an argument here, but otherwise Servo would crash due to
-    /// double borrows to return it.
-    fn existing_style_for_restyle_damage<'a>(&'a self,
-                                             current_computed_values: &'a ComputedValues,
-                                             pseudo: Option<&PseudoElement>)
-                                             -> Option<&'a PreExistingComputedValues>;
-
     /// Whether a given element may generate a pseudo-element.
     ///
     /// This is useful to avoid computing, for example, pseudo styles for
     /// `::-first-line` or `::-first-letter`, when we know it won't affect us.
     ///
     /// TODO(emilio, bz): actually implement the logic for it.
     fn may_generate_pseudo(
         &self,
--- a/servo/components/style/gecko/pseudo_element.rs
+++ b/servo/components/style/gecko/pseudo_element.rs
@@ -70,20 +70,32 @@ impl PseudoElement {
     }
 
     /// Creates a pseudo-element from an eager index.
     #[inline]
     pub fn from_eager_index(i: usize) -> Self {
         EAGER_PSEUDOS[i].clone()
     }
 
-    /// Whether this pseudo-element is ::before or ::after.
+    /// Whether the current pseudo element is ::before or ::after.
     #[inline]
     pub fn is_before_or_after(&self) -> bool {
-        matches!(*self, PseudoElement::Before | PseudoElement::After)
+        self.is_before() || self.is_after()
+    }
+
+    /// Whether this pseudo-element is the ::before pseudo.
+    #[inline]
+    pub fn is_before(&self) -> bool {
+        *self == PseudoElement::Before
+    }
+
+    /// Whether this pseudo-element is the ::after pseudo.
+    #[inline]
+    pub fn is_after(&self) -> bool {
+        *self == PseudoElement::After
     }
 
     /// Whether this pseudo-element is ::first-letter.
     #[inline]
     pub fn is_first_letter(&self) -> bool {
         *self == PseudoElement::FirstLetter
     }
 
--- a/servo/components/style/gecko/restyle_damage.rs
+++ b/servo/components/style/gecko/restyle_damage.rs
@@ -1,17 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 //! Gecko's restyle damage computation (aka change hints, aka `nsChangeHint`).
 
 use gecko_bindings::bindings;
 use gecko_bindings::structs;
-use gecko_bindings::structs::{nsChangeHint, nsStyleContext, nsStyleStructID};
+use gecko_bindings::structs::nsChangeHint;
 use matching::{StyleChange, StyleDifference};
 use properties::ComputedValues;
 use std::ops::{BitAnd, BitOr, BitOrAssign, Not};
 
 /// The representation of Gecko's restyle damage is just a wrapper over
 /// `nsChangeHint`.
 #[derive(Clone, Copy, Debug, PartialEq)]
 pub struct GeckoRestyleDamage(nsChangeHint);
@@ -41,59 +41,32 @@ impl GeckoRestyleDamage {
     /// given an old style (in the form of a `nsStyleContext`, and a new style
     /// (in the form of `ComputedValues`).
     ///
     /// Note that we could in theory just get two `ComputedValues` here and diff
     /// them, but Gecko has an interesting optimization when they mark accessed
     /// structs, so they effectively only diff structs that have ever been
     /// accessed from layout.
     pub fn compute_style_difference(
-        source: &nsStyleContext,
         old_style: &ComputedValues,
         new_style: &ComputedValues,
     ) -> StyleDifference {
         let mut any_style_changed: bool = false;
         let hint = unsafe {
-            bindings::Gecko_CalcStyleDifference(old_style,
-                                                new_style,
-                                                source.mBits,
-                                                &mut any_style_changed)
+            bindings::Gecko_CalcStyleDifference(
+                old_style,
+                new_style,
+                structs::NS_STYLE_INHERIT_MASK as u64,
+                &mut any_style_changed
+            )
         };
         let change = if any_style_changed { StyleChange::Changed } else { StyleChange::Unchanged };
         StyleDifference::new(GeckoRestyleDamage(nsChangeHint(hint)), change)
     }
 
-    /// Computes the `StyleDifference` between the two `ComputedValues` objects
-    /// for the case where the old and new style are both `display: none`.
-    ///
-    /// In general we don't need to generate damage for such elements, but we
-    /// do need to generate a frame reconstruction for `-moz-binding` changes,
-    /// so that we can start loading the new binding.
-    pub fn compute_undisplayed_style_difference(
-        old_style: &ComputedValues,
-        new_style: &ComputedValues,
-    ) -> StyleDifference {
-        let mut any_style_changed: bool = false;
-
-        // Just compute the Display struct's difference.
-        let display_struct_bit = 1 << (nsStyleStructID::eStyleStruct_Display as u32);
-        let hint = unsafe {
-            bindings::Gecko_CalcStyleDifference(old_style,
-                                                new_style,
-                                                display_struct_bit,
-                                                &mut any_style_changed)
-        };
-
-        // Only pay attention to a reconstruct change hint.
-        let damage = GeckoRestyleDamage(nsChangeHint(hint)) & Self::reconstruct();
-
-        let change = if damage.is_empty() { StyleChange::Changed } else { StyleChange::Unchanged };
-        StyleDifference::new(damage, change)
-    }
-
     /// Returns true if this restyle damage contains all the damage of |other|.
     pub fn contains(self, other: Self) -> bool {
         self & other == other
     }
 
     /// Gets restyle damage to reconstruct the entire frame, subsuming all
     /// other damage.
     pub fn reconstruct() -> Self {
--- a/servo/components/style/gecko/wrapper.rs
+++ b/servo/components/style/gecko/wrapper.rs
@@ -40,27 +40,26 @@ use gecko_bindings::bindings::Gecko_Elem
 use gecko_bindings::bindings::Gecko_ElementHasCSSAnimations;
 use gecko_bindings::bindings::Gecko_ElementHasCSSTransitions;
 use gecko_bindings::bindings::Gecko_GetActiveLinkAttrDeclarationBlock;
 use gecko_bindings::bindings::Gecko_GetAnimationRule;
 use gecko_bindings::bindings::Gecko_GetExtraContentStyleDeclarations;
 use gecko_bindings::bindings::Gecko_GetHTMLPresentationAttrDeclarationBlock;
 use gecko_bindings::bindings::Gecko_GetSMILOverrideDeclarationBlock;
 use gecko_bindings::bindings::Gecko_GetStyleAttrDeclarationBlock;
-use gecko_bindings::bindings::Gecko_GetStyleContext;
 use gecko_bindings::bindings::Gecko_GetUnvisitedLinkAttrDeclarationBlock;
 use gecko_bindings::bindings::Gecko_GetVisitedLinkAttrDeclarationBlock;
 use gecko_bindings::bindings::Gecko_IsSignificantChild;
 use gecko_bindings::bindings::Gecko_MatchLang;
 use gecko_bindings::bindings::Gecko_MatchStringArgPseudo;
 use gecko_bindings::bindings::Gecko_UnsetDirtyStyleAttr;
 use gecko_bindings::bindings::Gecko_UpdateAnimations;
 use gecko_bindings::structs;
 use gecko_bindings::structs::{RawGeckoElement, RawGeckoNode, RawGeckoXBLBinding};
-use gecko_bindings::structs::{nsIAtom, nsIContent, nsINode_BooleanFlag, nsStyleContext};
+use gecko_bindings::structs::{nsIAtom, nsIContent, nsINode_BooleanFlag};
 use gecko_bindings::structs::ELEMENT_HANDLED_SNAPSHOT;
 use gecko_bindings::structs::ELEMENT_HAS_ANIMATION_ONLY_DIRTY_DESCENDANTS_FOR_SERVO;
 use gecko_bindings::structs::ELEMENT_HAS_DIRTY_DESCENDANTS_FOR_SERVO;
 use gecko_bindings::structs::ELEMENT_HAS_SNAPSHOT;
 use gecko_bindings::structs::EffectCompositor_CascadeLevel as CascadeLevel;
 use gecko_bindings::structs::NODE_DESCENDANTS_NEED_FRAMES;
 use gecko_bindings::structs::nsChangeHint;
 use gecko_bindings::structs::nsIDocument_DocumentTheme as DocumentTheme;
@@ -1015,28 +1014,16 @@ impl<'le> TElement for GeckoElement<'le>
     fn each_class<F>(&self, callback: F)
         where F: FnMut(&Atom)
     {
         snapshot_helpers::each_class(self.0,
                                      callback,
                                      Gecko_ClassOrClassList)
     }
 
-    fn existing_style_for_restyle_damage<'a>(&'a self,
-                                             _existing_values: &'a ComputedValues,
-                                             pseudo: Option<&PseudoElement>)
-                                             -> Option<&'a nsStyleContext> {
-        // TODO(emilio): Migrate this to CSSPseudoElementType.
-        let atom_ptr = pseudo.map_or(ptr::null_mut(), |p| p.atom().as_ptr());
-        unsafe {
-            let context_ptr = Gecko_GetStyleContext(self.0, atom_ptr);
-            context_ptr.as_ref()
-        }
-    }
-
     fn has_snapshot(&self) -> bool {
         self.flags() & (ELEMENT_HAS_SNAPSHOT as u32) != 0
     }
 
     fn handled_snapshot(&self) -> bool {
         self.flags() & (ELEMENT_HANDLED_SNAPSHOT as u32) != 0
     }
 
--- a/servo/components/style/invalidation/element/invalidator.rs
+++ b/servo/components/style/invalidation/element/invalidator.rs
@@ -590,24 +590,37 @@ impl<'a, 'b: 'a, E> TreeStyleInvalidator
                 invalidated_self = true;
             }
             CompoundSelectorMatchingResult::Matched { next_combinator_offset } => {
                 let next_combinator =
                     invalidation.selector.combinator_at(next_combinator_offset);
                 matched = true;
 
                 if matches!(next_combinator, Combinator::PseudoElement) {
+                    // This will usually be the very next component, except for
+                    // the fact that we store compound selectors the other way
+                    // around, so there could also be state pseudo-classes.
                     let pseudo_selector =
                         invalidation.selector
                             .iter_raw_parse_order_from(next_combinator_offset - 1)
+                            .skip_while(|c| matches!(**c, Component::NonTSPseudoClass(..)))
                             .next()
                             .unwrap();
+
                     let pseudo = match *pseudo_selector {
                         Component::PseudoElement(ref pseudo) => pseudo,
-                        _ => unreachable!("Someone seriously messed up selector parsing"),
+                        _ => {
+                            unreachable!(
+                                "Someone seriously messed up selector parsing: \
+                                {:?} at offset {:?}: {:?}",
+                                invalidation.selector,
+                                next_combinator_offset,
+                                pseudo_selector,
+                            )
+                        }
                     };
 
                     // FIXME(emilio): This is not ideal, and could not be
                     // accurate if we ever have stateful element-backed eager
                     // pseudos.
                     //
                     // Ideally, we'd just remove element-backed eager pseudos
                     // altogether, given they work fine without it. Only gotcha
--- a/servo/components/style/matching.rs
+++ b/servo/components/style/matching.rs
@@ -9,17 +9,16 @@
 
 use context::{ElementCascadeInputs, SelectorFlagsMap, SharedStyleContext, StyleContext};
 use data::{ElementData, ElementStyles, RestyleData};
 use dom::TElement;
 use invalidation::element::restyle_hints::{RESTYLE_CSS_ANIMATIONS, RESTYLE_CSS_TRANSITIONS};
 use invalidation::element::restyle_hints::{RESTYLE_SMIL, RESTYLE_STYLE_ATTRIBUTE};
 use invalidation::element::restyle_hints::RestyleHint;
 use properties::ComputedValues;
-use properties::longhands::display::computed_value as display;
 use rule_tree::{CascadeLevel, StrongRuleNode};
 use selector_parser::{PseudoElement, RestyleDamage};
 use selectors::matching::ElementSelectorFlags;
 use servo_arc::{Arc, ArcBorrow};
 use traversal_flags;
 
 /// Represents the result of comparing an element's old and new style.
 pub struct StyleDifference {
@@ -147,60 +146,65 @@ trait PrivateMatchMethods: TElement {
         let style =
             StyleResolverForElement::new(*self, context, RuleInclusion::All)
                 .cascade_style_and_visited_with_default_parents(inputs);
 
         Some(style)
     }
 
     #[cfg(feature = "gecko")]
-    fn needs_animations_update(&self,
-                               context: &mut StyleContext<Self>,
-                               old_values: Option<&Arc<ComputedValues>>,
-                               new_values: &ComputedValues)
-                               -> bool {
+    fn needs_animations_update(
+        &self,
+        context: &mut StyleContext<Self>,
+        old_values: Option<&Arc<ComputedValues>>,
+        new_values: &ComputedValues,
+    ) -> bool {
+        use properties::longhands::display::computed_value as display;
+
         let new_box_style = new_values.get_box();
-        let has_new_animation_style = new_box_style.animation_name_count() >= 1 &&
-                                      new_box_style.animation_name_at(0).0.is_some();
+        let has_new_animation_style = new_box_style.specifies_animations();
         let has_animations = self.has_css_animations();
 
         old_values.map_or(has_new_animation_style, |old| {
             let old_box_style = old.get_box();
             let old_display_style = old_box_style.clone_display();
             let new_display_style = new_box_style.clone_display();
 
             // If the traverse is triggered by CSS rule changes, we need to
             // try to update all CSS animations on the element if the element
             // has CSS animation style regardless of whether the animation is
             // running or not.
             // TODO: We should check which @keyframes changed/added/deleted
             // and update only animations corresponding to those @keyframes.
             (context.shared.traversal_flags.contains(traversal_flags::ForCSSRuleChanges) &&
              has_new_animation_style) ||
-            !old_box_style.animations_equals(&new_box_style) ||
+            !old_box_style.animations_equals(new_box_style) ||
              (old_display_style == display::T::none &&
               new_display_style != display::T::none &&
               has_new_animation_style) ||
              (old_display_style != display::T::none &&
               new_display_style == display::T::none &&
               has_animations)
         })
     }
 
     /// Create a SequentialTask for resolving descendants in a SMIL display property
     /// animation if the display property changed from none.
     #[cfg(feature = "gecko")]
-    fn handle_display_change_for_smil_if_needed(&self,
-                                                context: &mut StyleContext<Self>,
-                                                old_values: Option<&ComputedValues>,
-                                                new_values: &ComputedValues,
-                                                restyle_hints: RestyleHint) {
+    fn handle_display_change_for_smil_if_needed(
+        &self,
+        context: &mut StyleContext<Self>,
+        old_values: Option<&ComputedValues>,
+        new_values: &ComputedValues,
+        restyle_hints: RestyleHint
+    ) {
         use context::DISPLAY_CHANGED_FROM_NONE_FOR_SMIL;
+        use properties::longhands::display::computed_value as display;
 
-        let display_changed_from_none = old_values.as_ref().map_or(false, |old| {
+        let display_changed_from_none = old_values.map_or(false, |old| {
             let old_display_style = old.get_box().clone_display();
             let new_display_style = new_values.get_box().clone_display();
             old_display_style == display::T::none &&
             new_display_style != display::T::none
         });
 
         if display_changed_from_none {
           // When display value is changed from none to other, we need
@@ -664,29 +668,32 @@ pub trait MatchMethods : TElement {
             Some(v) => v,
             None => return ChildCascadeRequirement::MustCascadeChildren,
         };
 
         // ::before and ::after are element-backed in Gecko, so they do the
         // damage calculation for themselves, when there's an actual pseudo.
         let is_existing_before_or_after =
             cfg!(feature = "gecko") &&
-            pseudo.map_or(false, |p| p.is_before_or_after()) &&
-            self.existing_style_for_restyle_damage(old_values, pseudo)
-                .is_some();
+            pseudo.map_or(false, |p| {
+                (p.is_before() && self.before_pseudo_element().is_some()) ||
+                (p.is_after() && self.after_pseudo_element().is_some())
+            });
 
         if is_existing_before_or_after {
             return ChildCascadeRequirement::CanSkipCascade;
         }
 
-        self.accumulate_damage_for(shared_context,
-                                   restyle,
-                                   old_values,
-                                   new_values,
-                                   pseudo)
+        self.accumulate_damage_for(
+            shared_context,
+            restyle,
+            old_values,
+            new_values,
+            pseudo
+        )
     }
 
     /// Updates the rule nodes without re-running selector matching, using just
     /// the rule tree.
     ///
     /// Returns true if an !important rule was replaced.
     fn replace_rules(
         &self,
@@ -816,87 +823,13 @@ pub trait MatchMethods : TElement {
     /// kind of layout or painting operations we'll need.
     fn compute_style_difference(
         &self,
         old_values: &ComputedValues,
         new_values: &ComputedValues,
         pseudo: Option<&PseudoElement>
     ) -> StyleDifference {
         debug_assert!(pseudo.map_or(true, |p| p.is_eager()));
-        if let Some(source) = self.existing_style_for_restyle_damage(old_values, pseudo) {
-            return RestyleDamage::compute_style_difference(source, old_values, new_values)
-        }
-
-        let new_display = new_values.get_box().clone_display();
-        let old_display = old_values.get_box().clone_display();
-
-        let new_style_is_display_none = new_display == display::T::none;
-        let old_style_is_display_none = old_display == display::T::none;
-
-        // If there's no style source, that likely means that Gecko couldn't
-        // find a style context.
-        //
-        // This happens with display:none elements, and not-yet-existing
-        // pseudo-elements.
-        if new_style_is_display_none && old_style_is_display_none {
-            // The style remains display:none.  The only case we need to care
-            // about is if -moz-binding changed, and to generate a reconstruct
-            // so that we can start the binding load.  Otherwise, there is no
-            // need for damage.
-            return RestyleDamage::compute_undisplayed_style_difference(old_values, new_values);
-        }
-
-        if pseudo.map_or(false, |p| p.is_before_or_after()) {
-            // FIXME(bz) This duplicates some of the logic in
-            // PseudoElement::should_exist, but it's not clear how best to share
-            // that logic without redoing the "get the display" work.
-            let old_style_generates_no_pseudo =
-                old_style_is_display_none ||
-                old_values.ineffective_content_property();
-
-            let new_style_generates_no_pseudo =
-                new_style_is_display_none ||
-                new_values.ineffective_content_property();
-
-            if old_style_generates_no_pseudo != new_style_generates_no_pseudo {
-                return StyleDifference::new(RestyleDamage::reconstruct(), StyleChange::Changed)
-            }
-
-            // The pseudo-element will remain undisplayed, so just avoid
-            // triggering any change.
-            //
-            // NOTE(emilio): We will only arrive here for pseudo-elements that
-            // aren't generated (see the is_existing_before_or_after check in
-            // accumulate_damage).
-            //
-            // However, it may be the case that the style of this element would
-            // make us think we need a pseudo, but we don't, like for pseudos in
-            // replaced elements, that's why we need the old != new instead of
-            // just check whether the new style would generate a pseudo.
-            return StyleDifference::new(RestyleDamage::empty(), StyleChange::Unchanged)
-        }
-
-        if pseudo.map_or(false, |p| p.is_first_letter() || p.is_first_line()) {
-            // No one cares about this pseudo, and we've checked above that
-            // we're not switching from a "cares" to a "doesn't care" state
-            // or vice versa.
-            return StyleDifference::new(RestyleDamage::empty(),
-                                        StyleChange::Unchanged)
-        }
-
-        // If we are changing display property we need to accumulate
-        // reconstruction damage for the change.
-        // FIXME: Bug 1378972: This is a workaround for bug 1374175, we should
-        // generate more accurate restyle damage in fallback cases.
-        let needs_reconstruction = new_display != old_display;
-        let damage = if needs_reconstruction {
-            RestyleDamage::reconstruct()
-        } else {
-            RestyleDamage::empty()
-        };
-        // We don't really know if there was a change in any style (since we
-        // didn't actually call compute_style_difference) but we return
-        // StyleChange::Changed conservatively.
-        StyleDifference::new(damage, StyleChange::Changed)
+        RestyleDamage::compute_style_difference(old_values, new_values)
     }
 }
 
 impl<E: TElement> MatchMethods for E {}
--- a/servo/components/style/properties/helpers/animated_properties.mako.rs
+++ b/servo/components/style/properties/helpers/animated_properties.mako.rs
@@ -1335,18 +1335,19 @@ fn build_identity_transform_list(list: &
             TransformOperation::Translate(..) => {
                 result.push(TransformOperation::Translate(LengthOrPercentage::zero(),
                                                           LengthOrPercentage::zero(),
                                                           Au(0)));
             }
             TransformOperation::Scale(..) => {
                 result.push(TransformOperation::Scale(1.0, 1.0, 1.0));
             }
-            TransformOperation::Rotate(..) => {
-                result.push(TransformOperation::Rotate(0.0, 0.0, 1.0, Angle::zero()));
+            TransformOperation::Rotate(x, y, z, a) => {
+                let (x, y, z, _) = get_normalized_vector_and_angle(x, y, z, a);
+                result.push(TransformOperation::Rotate(x, y, z, Angle::zero()));
             }
             TransformOperation::Perspective(..) |
             TransformOperation::AccumulateMatrix { .. } |
             TransformOperation::InterpolateMatrix { .. } => {
                 // Perspective: We convert a perspective function into an equivalent
                 //     ComputedMatrix, and then decompose/interpolate/recompose these matrices.
                 // AccumulateMatrix/InterpolateMatrix: We do interpolation on
                 //     AccumulateMatrix/InterpolateMatrix by reading it as a ComputedMatrix
@@ -1420,21 +1421,19 @@ fn add_weighted_transform_lists(from_lis
                     let iy = add_weighted_with_initial_val(&fy, &ty, self_portion,
                                                            other_portion, &1.0).unwrap();
                     let iz = add_weighted_with_initial_val(&fz, &tz, self_portion,
                                                            other_portion, &1.0).unwrap();
                     result.push(TransformOperation::Scale(ix, iy, iz));
                 }
                 (&TransformOperation::Rotate(fx, fy, fz, fa),
                  &TransformOperation::Rotate(tx, ty, tz, ta)) => {
-                    let norm_f = ((fx * fx) + (fy * fy) + (fz * fz)).sqrt();
-                    let norm_t = ((tx * tx) + (ty * ty) + (tz * tz)).sqrt();
-                    let (fx, fy, fz) = (fx / norm_f, fy / norm_f, fz / norm_f);
-                    let (tx, ty, tz) = (tx / norm_t, ty / norm_t, tz / norm_t);
-                    if fx == tx && fy == ty && fz == tz {
+                    let (fx, fy, fz, fa) = get_normalized_vector_and_angle(fx, fy, fz, fa);
+                    let (tx, ty, tz, ta) = get_normalized_vector_and_angle(tx, ty, tz, ta);
+                    if (fx, fy, fz) == (tx, ty, tz) {
                         let ia = fa.add_weighted(&ta, self_portion, other_portion).unwrap();
                         result.push(TransformOperation::Rotate(fx, fy, fz, ia));
                     } else {
                         let matrix_f = rotate_to_matrix(fx, fy, fz, fa);
                         let matrix_t = rotate_to_matrix(tx, ty, tz, ta);
                         let sum = matrix_f.add_weighted(&matrix_t, self_portion, other_portion)
                                           .unwrap();
 
@@ -1877,18 +1876,18 @@ impl ComputeSquaredDistance for Quaterni
         let distance = self.dot(other).max(-1.0).min(1.0).acos() * 2.0;
         Ok(SquaredDistance::Value(distance * distance))
     }
 }
 
 impl DirectionVector {
     /// Create a DirectionVector.
     #[inline]
-    fn new(x: f64, y: f64, z: f64) -> Self {
-        DirectionVector(Point3D::new(x, y, z))
+    fn new(x: f32, y: f32, z: f32) -> Self {
+        DirectionVector(Point3D::new(x as f64, y as f64, z as f64))
     }
 
     /// Return the normalized direction vector.
     #[inline]
     fn normalize(&mut self) -> bool {
         let len = self.length();
         if len > 0. {
             self.0.x = self.0.x / len;
@@ -1902,16 +1901,29 @@ impl DirectionVector {
 
     /// Get the length of this vector.
     #[inline]
     fn length(&self) -> f64 {
         self.0.to_array().iter().fold(0f64, |sum, v| sum + v * v).sqrt()
     }
 }
 
+/// Return the normalized direction vector and its angle.
+// A direction vector that cannot be normalized, such as [0,0,0], will cause the
+// rotation to not be applied. i.e. Use an identity matrix or rotate3d(0, 0, 1, 0).
+fn get_normalized_vector_and_angle(x: f32, y: f32, z: f32, angle: Angle)
+                                   -> (f32, f32, f32, Angle) {
+    let mut vector = DirectionVector::new(x, y, z);
+    if vector.normalize() {
+        (vector.0.x as f32, vector.0.y as f32, vector.0.z as f32, angle)
+    } else {
+        (0., 0., 1., Angle::zero())
+    }
+}
+
 /// Decompose a 3D matrix.
 /// https://drafts.csswg.org/css-transforms/#decomposing-a-3d-matrix
 fn decompose_3d_matrix(mut matrix: ComputedMatrix) -> Result<MatrixDecomposed3D, ()> {
     // Normalize the matrix.
     if matrix.m44 == 0.0 {
         return Err(());
     }
 
@@ -2534,35 +2546,25 @@ fn compute_transform_lists_squared_dista
             (&TransformOperation::Scale(fx, fy, fz),
              &TransformOperation::Scale(tx, ty, tz)) => {
                 fx.compute_squared_distance(&tx).unwrap_or(zero_distance) +
                     fy.compute_squared_distance(&ty).unwrap_or(zero_distance) +
                     fz.compute_squared_distance(&tz).unwrap_or(zero_distance)
             }
             (&TransformOperation::Rotate(fx, fy, fz, fa),
              &TransformOperation::Rotate(tx, ty, tz, ta)) => {
-                // A direction vector that cannot be normalized, such as [0,0,0], will cause the
-                // rotation to not be applied. i.e. Use an identity matrix or rotate3d(0, 0, 1, 0).
-                let get_normalized_vector_and_angle = |x: f32, y: f32, z: f32, angle: Angle|
-                                                      -> (DirectionVector, Angle) {
-                    let mut vector = DirectionVector::new(x as f64, y as f64, z as f64);
-                    if vector.normalize() {
-                        (vector, angle)
-                    } else {
-                        (DirectionVector::new(0., 0., 1.), Angle::zero())
-                    }
-                };
-
-                let (vector1, angle1) = get_normalized_vector_and_angle(fx, fy, fz, fa);
-                let (vector2, angle2) = get_normalized_vector_and_angle(tx, ty, tz, ta);
-                if vector1 == vector2 {
+                let (fx, fy, fz, angle1) = get_normalized_vector_and_angle(fx, fy, fz, fa);
+                let (tx, ty, tz, angle2) = get_normalized_vector_and_angle(tx, ty, tz, ta);
+                if (fx, fy, fz) == (tx, ty, tz) {
                     angle1.compute_squared_distance(&angle2).unwrap_or(zero_distance)
                 } else {
-                    let q1 = Quaternion::from_direction_and_angle(&vector1, angle1.radians64());
-                    let q2 = Quaternion::from_direction_and_angle(&vector2, angle2.radians64());
+                    let v1 = DirectionVector::new(fx, fy, fz);
+                    let v2 = DirectionVector::new(tx, ty, tz);
+                    let q1 = Quaternion::from_direction_and_angle(&v1, angle1.radians64());
+                    let q2 = Quaternion::from_direction_and_angle(&v2, angle2.radians64());
                     q1.compute_squared_distance(&q2).unwrap_or(zero_distance)
                 }
             }
             (&TransformOperation::Perspective(fd),
              &TransformOperation::Perspective(td)) => {
                 let mut fd_matrix = ComputedMatrix::identity();
                 let mut td_matrix = ComputedMatrix::identity();
                 if fd.0 > 0 {
--- a/servo/components/style/selector_parser.rs
+++ b/servo/components/style/selector_parser.rs
@@ -30,26 +30,16 @@ pub use servo::selector_parser::ServoEle
 pub use gecko::snapshot::GeckoElementSnapshot as Snapshot;
 
 #[cfg(feature = "servo")]
 pub use servo::restyle_damage::ServoRestyleDamage as RestyleDamage;
 
 #[cfg(feature = "gecko")]
 pub use gecko::restyle_damage::GeckoRestyleDamage as RestyleDamage;
 
-/// A type that represents the previous computed values needed for restyle
-/// damage calculation.
-#[cfg(feature = "servo")]
-pub type PreExistingComputedValues = ::properties::ComputedValues;
-
-/// A type that represents the previous computed values needed for restyle
-/// damage calculation.
-#[cfg(feature = "gecko")]
-pub type PreExistingComputedValues = ::gecko_bindings::structs::nsStyleContext;
-
 /// Servo's selector parser.
 #[cfg_attr(feature = "servo", derive(HeapSizeOf))]
 pub struct SelectorParser<'a> {
     /// The origin of the stylesheet we're parsing.
     pub stylesheet_origin: Origin,
     /// The namespace set of the stylesheet.
     pub namespaces: &'a Namespaces,
     /// The extra URL data of the stylesheet, which is used to look up
--- a/servo/components/style/servo/restyle_damage.rs
+++ b/servo/components/style/servo/restyle_damage.rs
@@ -55,36 +55,25 @@ bitflags! {
 
 impl HeapSizeOf for ServoRestyleDamage {
     fn heap_size_of_children(&self) -> usize { 0 }
 }
 
 impl ServoRestyleDamage {
     /// Compute the `StyleDifference` (including the appropriate restyle damage)
     /// for a given style change between `old` and `new`.
-    pub fn compute_style_difference(_source: &ComputedValues,
-                                    old: &ComputedValues,
-                                    new: &ComputedValues)
-                                    -> StyleDifference {
+    pub fn compute_style_difference(
+        old: &ComputedValues,
+        new: &ComputedValues,
+    ) -> StyleDifference {
         let damage = compute_damage(old, new);
         let change = if damage.is_empty() { StyleChange::Unchanged } else { StyleChange::Changed };
         StyleDifference::new(damage, change)
     }
 
-    /// Computes the `StyleDifference` between the two `ComputedValues` objects
-    /// for the case where the old and new style are both `display: none`.
-    ///
-    /// For Servo we never need to generate any damage for such elements.
-    pub fn compute_undisplayed_style_difference(
-        _old_style: &ComputedValues,
-        _new_style: &ComputedValues,
-    ) -> StyleDifference {
-        StyleDifference::new(Self::empty(), StyleChange::Unchanged)
-    }
-
     /// Returns a bitmask that represents a flow that needs to be rebuilt and
     /// reflowed.
     ///
     /// FIXME(bholley): Do we ever actually need this? Shouldn't
     /// RECONSTRUCT_FLOW imply everything else?
     pub fn rebuild_and_reflow() -> ServoRestyleDamage {
         REPAINT | REPOSITION | STORE_OVERFLOW | BUBBLE_ISIZES | REFLOW_OUT_OF_FLOW | REFLOW |
             RECONSTRUCT_FLOW
--- a/servo/components/style/servo/selector_parser.rs
+++ b/servo/components/style/servo/selector_parser.rs
@@ -125,20 +125,32 @@ impl PseudoElement {
     #[inline]
     pub fn from_eager_index(i: usize) -> Self {
         assert!(i < EAGER_PSEUDO_COUNT);
         let result: PseudoElement = unsafe { mem::transmute(i) };
         debug_assert!(result.is_eager());
         result
     }
 
-    /// Whether the current pseudo element is :before or :after.
+    /// Whether the current pseudo element is ::before or ::after.
     #[inline]
     pub fn is_before_or_after(&self) -> bool {
-        matches!(*self, PseudoElement::After | PseudoElement::Before)
+        self.is_before() || self.is_after()
+    }
+
+    /// Whether this pseudo-element is the ::before pseudo.
+    #[inline]
+    pub fn is_before(&self) -> bool {
+        *self == PseudoElement::Before
+    }
+
+    /// Whether this pseudo-element is the ::after pseudo.
+    #[inline]
+    pub fn is_after(&self) -> bool {
+        *self == PseudoElement::After
     }
 
     /// Whether the current pseudo element is :first-letter
     #[inline]
     pub fn is_first_letter(&self) -> bool {
         false
     }
 
--- a/taskcluster/taskgraph/actions/registry.py
+++ b/taskcluster/taskgraph/actions/registry.py
@@ -5,16 +5,17 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import, print_function, unicode_literals
 
 import json
 import os
 import inspect
 import re
+from slugid import nice as slugid
 from mozbuild.util import memoize
 from types import FunctionType
 from collections import namedtuple
 from taskgraph import create
 from taskgraph.util.docker import docker_image
 from taskgraph.parameters import Parameters
 
 
@@ -178,30 +179,34 @@ def register_callback_action(name, title
                 return None
 
             match = re.match(r'https://(hg.mozilla.org)/(.*?)/?$', parameters['head_repository'])
             if not match:
                 raise Exception('Unrecognized head_repository')
             repo_scope = 'assume:repo:{}/{}:*'.format(
                 match.group(1), match.group(2))
 
+            task_group_id = os.environ.get('TASK_ID', slugid())
+
             return {
                 'created': {'$fromNow': ''},
                 'deadline': {'$fromNow': '12 hours'},
                 'expires': {'$fromNow': '14 days'},
                 'metadata': {
                     'owner': 'mozilla-taskcluster-maintenance@mozilla.com',
                     'source': '{}raw-file/{}/{}'.format(
                         parameters['head_repository'], parameters['head_rev'], source_path,
                     ),
                     'name': 'Action: {}'.format(title),
                     'description': 'Task executing callback for action.\n\n---\n' + description,
                 },
                 'workerType': 'gecko-{}-decision'.format(parameters['level']),
                 'provisionerId': 'aws-provisioner-v1',
+                'taskGroupId': task_group_id,
+                'schedulerId': 'gecko-level-{}'.format(parameters['level']),
                 'scopes': [
                     repo_scope,
                 ],
                 'tags': {
                     'createdForUser': parameters['owner'],
                     'kind': 'action-callback',
                 },
                 'routes': [
@@ -212,17 +217,17 @@ def register_callback_action(name, title
                 ],
                 'payload': {
                     'env': {
                         'GECKO_BASE_REPOSITORY': 'https://hg.mozilla.org/mozilla-unified',
                         'GECKO_HEAD_REPOSITORY': parameters['head_repository'],
                         'GECKO_HEAD_REF': parameters['head_ref'],
                         'GECKO_HEAD_REV': parameters['head_rev'],
                         'HG_STORE_PATH': '/home/worker/checkouts/hg-store',
-                        'ACTION_TASK_GROUP_ID': {'$eval': 'taskGroupId'},
+                        'ACTION_TASK_GROUP_ID': task_group_id,
                         'ACTION_TASK_ID': {'$json': {'$eval': 'taskId'}},
                         'ACTION_TASK': {'$json': {'$eval': 'task'}},
                         'ACTION_INPUT': {'$json': {'$eval': 'input'}},
                         'ACTION_CALLBACK': cb.__name__,
                         'ACTION_PARAMETERS': {'$json': {'$eval': 'parameters'}},
                     },
                     'cache': {
                         'level-{}-checkouts'.format(parameters['level']):
--- a/taskcluster/taskgraph/actions/retrigger.py
+++ b/taskcluster/taskgraph/actions/retrigger.py
@@ -1,68 +1,73 @@
 # -*- coding: utf-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/.
 
 from __future__ import absolute_import, print_function, unicode_literals
 
-from .registry import register_task_action
+import logging
+
+from .util import (
+    create_tasks,
+    find_decision_task
+)
+from .registry import register_callback_action
+from taskgraph.util.taskcluster import get_artifact
+from taskgraph.taskgraph import TaskGraph
+
+logger = logging.getLogger(__name__)
 
 
-@register_task_action(
+@register_callback_action(
     title='Retrigger',
     name='retrigger',
+    symbol='rt',
     description=(
         'Create a clone of the task.\n\n'
-        'This does not update any dependencies or '
-        'cause any downstream tasks to be retriggered.'
     ),
     order=1,
     context=[{}],
+    schema={
+        'type': 'object',
+        'properties': {
+            'downstream': {
+                'type': 'boolean',
+                'description': (
+                    'If true, downstream tasks from this one will be cloned as well. '
+                    'The dependencies will be updated to work with the new task at the root.'
+                ),
+                'default': False,
+            },
+            'times': {
+                'type': 'integer',
+                'default': 1,
+                'minimum': 1,
+                'maximum': 6,
+                'title': 'Times',
+                'description': 'How many times to run each task.',
+            }
+        }
+    }
 )
-def retrigger_task_builder(parameters):
-
-    new_expires = '30 days'
+def retrigger_action(parameters, input, task_group_id, task_id, task):
+    decision_task_id = find_decision_task(parameters)
 
-    return {
-        '$merge': [
-            {'$eval': 'task'},
-            {'created': {'$fromNow': ''}},
-            {'deadline': {'$fromNow': '1 day'}},
-            {'expires': {'$fromNow': new_expires}},
-            {'payload': {
-                '$merge': [
-                    {'$eval': 'task.payload'},
-                    {
-                        '$if': '"artifacts" in task.payload',
-                        'then': {
-                            'artifacts': {
-                                '$if': 'typeof(task.payload.artifacts) == "object"',
-                                'then': {
-                                    '$map': {'$eval': 'task.payload.artifacts'},
-                                    'each(artifact)': {
-                                        '${artifact.key}': {
-                                            '$merge': [
-                                                {'$eval': 'artifact.val'},
-                                                {'expires': {'$fromNow': new_expires}},
-                                            ],
-                                        },
-                                    },
-                                },
-                                'else': {
-                                    '$map': {'$eval': 'task.payload.artifacts'},
-                                    'each(artifact)': {
-                                        '$merge': [
-                                            {'$eval': 'artifact'},
-                                            {'expires': {'$fromNow': new_expires}},
-                                        ],
-                                    },
-                                },
-                            },
-                        },
-                        'else': {},
-                    }
-                ]
-            }}
-        ]
-    }
+    full_task_graph = get_artifact(decision_task_id, "public/full-task-graph.json")
+    _, full_task_graph = TaskGraph.from_json(full_task_graph)
+    label_to_taskid = get_artifact(decision_task_id, "public/label-to-taskid.json")
+
+    label = task['metadata']['name']
+    with_downstream = ' '
+    to_run = [label]
+
+    if input.get('downstream'):
+        to_run = full_task_graph.graph.transitive_closure(set(to_run), reverse=True).nodes
+        to_run = to_run & set(label_to_taskid.keys())
+        with_downstream = ' (with downstream) '
+
+    times = input.get('times', 1)
+    for i in xrange(times):
+        create_tasks(to_run, full_task_graph, label_to_taskid, parameters, decision_task_id)
+
+        logger.info('Scheduled {}{}(time {}/{})'.format(label, with_downstream, i+1, times))
--- a/taskcluster/taskgraph/graph.py
+++ b/taskcluster/taskgraph/graph.py
@@ -35,35 +35,50 @@ class Graph(object):
         self.edges = edges
 
     def __eq__(self, other):
         return self.nodes == other.nodes and self.edges == other.edges
 
     def __repr__(self):
         return "<Graph nodes={!r} edges={!r}>".format(self.nodes, self.edges)
 
-    def transitive_closure(self, nodes):
+    def transitive_closure(self, nodes, reverse=False):
         """
         Return the transitive closure of <nodes>: the graph containing all
         specified nodes as well as any nodes reachable from them, and any
         intervening edges.
+
+        If `reverse` is true, the "reachability" will be reversed and this
+        will return the set of nodes that can reach the specified nodes.
+
+        Example
+        -------
+
+        a ------> b ------> c
+                  |
+                  `-------> d
+
+        transitive_closure([b]).nodes == set([a, b])
+        transitive_closure([c]).nodes == set([c, b, a])
+        transitive_closure([c], reverse=True).nodes == set([c])
+        transitive_closure([b], reverse=True).nodes == set([b, c, d])
         """
         assert isinstance(nodes, set)
         assert nodes <= self.nodes
 
         # generate a new graph by expanding along edges until reaching a fixed
         # point
         new_nodes, new_edges = nodes, set()
         nodes, edges = set(), set()
         while (new_nodes, new_edges) != (nodes, edges):
             nodes, edges = new_nodes, new_edges
             add_edges = set((left, right, name)
                             for (left, right, name) in self.edges
-                            if left in nodes)
-            add_nodes = set(right for (_, right, _) in add_edges)
+                            if (right if reverse else left) in nodes)
+            add_nodes = set((left if reverse else right) for (left, right, _) in add_edges)
             new_nodes = nodes | add_nodes
             new_edges = edges | add_edges
         return Graph(new_nodes, new_edges)
 
     def visit_postorder(self):
         """
         Generate a sequence of nodes in postorder, such that every node is
         visited *after* any nodes it links to.
--- a/toolkit/components/extensions/ext-downloads.js
+++ b/toolkit/components/extensions/ext-downloads.js
@@ -26,16 +26,18 @@ var {
 const DOWNLOAD_ITEM_FIELDS = ["id", "url", "referrer", "filename", "incognito",
                               "danger", "mime", "startTime", "endTime",
                               "estimatedEndTime", "state",
                               "paused", "canResume", "error",
                               "bytesReceived", "totalBytes",
                               "fileSize", "exists",
                               "byExtensionId", "byExtensionName"];
 
+const DOWNLOAD_DATE_FIELDS = ["startTime", "endTime", "estimatedEndTime"];
+
 // Fields that we generate onChanged events for.
 const DOWNLOAD_ITEM_CHANGE_FIELDS = ["endTime", "state", "paused", "canResume",
                                      "error", "exists"];
 
 // From https://fetch.spec.whatwg.org/#forbidden-header-name
 const FORBIDDEN_HEADERS = ["ACCEPT-CHARSET", "ACCEPT-ENCODING",
                            "ACCESS-CONTROL-REQUEST-HEADERS", "ACCESS-CONTROL-REQUEST-METHOD",
                            "CONNECTION", "CONTENT-LENGTH", "COOKIE", "COOKIE2", "DATE", "DNT",
@@ -55,17 +57,24 @@ class DownloadItem {
   get url() { return this.download.source.url; }
   get referrer() { return this.download.source.referrer; }
   get filename() { return this.download.target.path; }
   get incognito() { return this.download.source.isPrivate; }
   get danger() { return "safe"; } // TODO
   get mime() { return this.download.contentType; }
   get startTime() { return this.download.startTime; }
   get endTime() { return null; } // TODO
-  get estimatedEndTime() { return null; } // TODO
+  get estimatedEndTime() {
+    // Based on the code in summarizeDownloads() in DownloadsCommon.jsm
+    if (this.download.hasProgress && this.download.speed > 0) {
+      let sizeLeft = this.download.totalBytes - this.download.currentBytes;
+      let rawTimeLeft = sizeLeft / this.download.speed;
+      return new Date(Date.now() + rawTimeLeft);
+    }
+  }
   get state() {
     if (this.download.succeeded) {
       return "complete";
     }
     if (this.download.canceled) {
       return "interrupted";
     }
     return "in_progress";
@@ -115,18 +124,20 @@ class DownloadItem {
    * @returns {object} A DownloadItem with flat properties,
    *                   suitable for cloning.
    */
   serialize() {
     let obj = {};
     for (let field of DOWNLOAD_ITEM_FIELDS) {
       obj[field] = this[field];
     }
-    if (obj.startTime) {
-      obj.startTime = obj.startTime.toISOString();
+    for (let field of DOWNLOAD_DATE_FIELDS) {
+      if (obj[field]) {
+        obj[field] = obj[field].toISOString();
+      }
     }
     return obj;
   }
 
   // When a change event fires, handlers can look at how an individual
   // field changed by comparing item.fieldname with item.prechange.fieldname.
   // After all handlers have been invoked, this gets called to store the
   // current values of all fields ahead of the next event.
--- a/toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js
@@ -43,16 +43,23 @@ function handleRequest(request, response
       response.setHeader("Content-Range", `*/${TOTAL_LEN}`, false);
       response.finish();
       return;
     }
 
     response.setStatusLine(request.httpVersion, 206, "Partial Content");
     response.setHeader("Content-Range", `${start}-${end}/${TOTAL_LEN}`, false);
     response.write(TEST_DATA.slice(start, end + 1));
+  } else if (request.queryString.includes("stream")) {
+    response.processAsync();
+    response.setHeader("Content-Length", "10000", false);
+    response.write("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
+    setInterval(() => {
+      response.write("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
+    }, 50);
   } else {
     response.processAsync();
     response.setHeader("Content-Length", `${TOTAL_LEN}`, false);
     response.write(TEST_DATA.slice(0, PARTIAL_LEN));
   }
 
   do_register_cleanup(() => {
     try {
@@ -212,25 +219,25 @@ function runInExtension(what, ...args) {
   extension.sendMessage(`${what}.request`, ...args);
   return extension.awaitMessage(`${what}.done`);
 }
 
 // This is pretty simplistic, it looks for a progress update for a
 // download of the given url in which the total bytes are exactly equal
 // to the given value.  Unless you know exactly how data will arrive from
 // the server (eg see interruptible.sjs), it probably isn't very useful.
-async function waitForProgress(url, bytes) {
+async function waitForProgress(url, testFn) {
   let list = await Downloads.getList(Downloads.ALL);
 
   return new Promise(resolve => {
     const view = {
       onDownloadChanged(download) {
-        if (download.source.url == url && download.currentBytes == bytes) {
+        if (download.source.url == url && testFn(download.currentBytes)) {
           list.removeView(view);
-          resolve();
+          resolve(download.currentBytes);
         }
       },
     };
     list.addView(view);
   });
 }
 
 add_task(async function setup() {
@@ -288,17 +295,17 @@ add_task(async function test_events() {
 
 add_task(async function test_cancel() {
   let url = getInterruptibleUrl();
   do_print(url);
   let msg = await runInExtension("download", {url});
   equal(msg.status, "success", "download() succeeded");
   const id = msg.result;
 
-  let progressPromise = waitForProgress(url, INT_PARTIAL_LEN);
+  let progressPromise = waitForProgress(url, bytes => bytes == INT_PARTIAL_LEN);
 
   msg = await runInExtension("waitForEvents", [
     {type: "onCreated", data: {id}},
   ]);
   equal(msg.status, "success", "got created and changed events");
 
   await progressPromise;
   do_print(`download reached ${INT_PARTIAL_LEN} bytes`);
@@ -343,16 +350,17 @@ add_task(async function test_cancel() {
   equal(msg.status, "success", "got onChanged events corresponding to cancel()");
 
   msg = await runInExtension("search", {error: "USER_CANCELED"});
   equal(msg.status, "success", "search() succeeded");
   equal(msg.result.length, 1, "search() found 1 download");
   equal(msg.result[0].id, id, "download.id is correct");
   equal(msg.result[0].state, "interrupted", "download.state is correct");
   equal(msg.result[0].paused, false, "download.paused is correct");
+  equal(msg.result[0].estimatedEndTime, null, "download.estimatedEndTime is correct");
   equal(msg.result[0].canResume, false, "download.canResume is correct");
   equal(msg.result[0].error, "USER_CANCELED", "download.error is correct");
   equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
   equal(msg.result[0].exists, false, "download.exists is correct");
 
   msg = await runInExtension("pause", id);
   equal(msg.status, "error", "cannot pause a canceled download");
 
@@ -361,17 +369,17 @@ add_task(async function test_cancel() {
 });
 
 add_task(async function test_pauseresume() {
   let url = getInterruptibleUrl();
   let msg = await runInExtension("download", {url});
   equal(msg.status, "success", "download() succeeded");
   const id = msg.result;
 
-  let progressPromise = waitForProgress(url, INT_PARTIAL_LEN);
+  let progressPromise = waitForProgress(url, bytes => bytes == INT_PARTIAL_LEN);
 
   msg = await runInExtension("waitForEvents", [
     {type: "onCreated", data: {id}},
   ]);
   equal(msg.status, "success", "got created and changed events");
 
   await progressPromise;
   do_print(`download reached ${INT_PARTIAL_LEN} bytes`);
@@ -410,16 +418,17 @@ add_task(async function test_pauseresume
   equal(msg.status, "success", "got onChanged event corresponding to pause");
 
   msg = await runInExtension("search", {paused: true});
   equal(msg.status, "success", "search() succeeded");
   equal(msg.result.length, 1, "search() found 1 download");
   equal(msg.result[0].id, id, "download.id is correct");
   equal(msg.result[0].state, "interrupted", "download.state is correct");
   equal(msg.result[0].paused, true, "download.paused is correct");
+  equal(msg.result[0].estimatedEndTime, null, "download.estimatedEndTime is correct");
   equal(msg.result[0].canResume, true, "download.canResume is correct");
   equal(msg.result[0].error, "USER_CANCELED", "download.error is correct");
   equal(msg.result[0].bytesReceived, INT_PARTIAL_LEN, "download.bytesReceived is correct");
   equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
   equal(msg.result[0].exists, false, "download.exists is correct");
 
   msg = await runInExtension("search", {error: "USER_CANCELED"});
   equal(msg.status, "success", "search() succeeded");
@@ -468,16 +477,17 @@ add_task(async function test_pauseresume
   ]);
   equal(msg.status, "success", "got onChanged events for resume and complete");
 
   msg = await runInExtension("search", {id});
   equal(msg.status, "success", "search() succeeded");
   equal(msg.result.length, 1, "search() found 1 download");
   equal(msg.result[0].state, "complete", "download.state is correct");
   equal(msg.result[0].paused, false, "download.paused is correct");
+  equal(msg.result[0].estimatedEndTime, null, "download.estimatedEndTime is correct");
   equal(msg.result[0].canResume, false, "download.canResume is correct");
   equal(msg.result[0].error, null, "download.error is correct");
   equal(msg.result[0].bytesReceived, INT_TOTAL_LEN, "download.bytesReceived is correct");
   equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
   equal(msg.result[0].exists, true, "download.exists is correct");
 
   msg = await runInExtension("pause", id);
   equal(msg.status, "error", "cannot pause a completed download");
@@ -487,17 +497,17 @@ add_task(async function test_pauseresume
 });
 
 add_task(async function test_pausecancel() {
   let url = getInterruptibleUrl();
   let msg = await runInExtension("download", {url});
   equal(msg.status, "success", "download() succeeded");
   const id = msg.result;
 
-  let progressPromise = waitForProgress(url, INT_PARTIAL_LEN);
+  let progressPromise = waitForProgress(url, bytes => bytes == INT_PARTIAL_LEN);
 
   msg = await runInExtension("waitForEvents", [
     {type: "onCreated", data: {id}},
   ]);
   equal(msg.status, "success", "got created and changed events");
 
   await progressPromise;
   do_print(`download reached ${INT_PARTIAL_LEN} bytes`);
@@ -536,16 +546,17 @@ add_task(async function test_pausecancel
   equal(msg.status, "success", "got onChanged event corresponding to pause");
 
   msg = await runInExtension("search", {paused: true});
   equal(msg.status, "success", "search() succeeded");
   equal(msg.result.length, 1, "search() found 1 download");
   equal(msg.result[0].id, id, "download.id is correct");
   equal(msg.result[0].state, "interrupted", "download.state is correct");
   equal(msg.result[0].paused, true, "download.paused is correct");
+  equal(msg.result[0].estimatedEndTime, null, "download.estimatedEndTime is correct");
   equal(msg.result[0].canResume, true, "download.canResume is correct");
   equal(msg.result[0].error, "USER_CANCELED", "download.error is correct");
   equal(msg.result[0].bytesReceived, INT_PARTIAL_LEN, "download.bytesReceived is correct");
   equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
   equal(msg.result[0].exists, false, "download.exists is correct");
 
   msg = await runInExtension("search", {error: "USER_CANCELED"});
   equal(msg.status, "success", "search() succeeded");
@@ -573,16 +584,17 @@ add_task(async function test_pausecancel
   ]);
   equal(msg.status, "success", "got onChanged event for cancel");
 
   msg = await runInExtension("search", {id});
   equal(msg.status, "success", "search() succeeded");
   equal(msg.result.length, 1, "search() found 1 download");
   equal(msg.result[0].state, "interrupted", "download.state is correct");
   equal(msg.result[0].paused, false, "download.paused is correct");
+  equal(msg.result[0].estimatedEndTime, null, "download.estimatedEndTime is correct");
   equal(msg.result[0].canResume, false, "download.canResume is correct");
   equal(msg.result[0].error, "USER_CANCELED", "download.error is correct");
   equal(msg.result[0].totalBytes, INT_TOTAL_LEN, "download.totalBytes is correct");
   equal(msg.result[0].exists, false, "download.exists is correct");
 });
 
 add_task(async function test_pause_resume_cancel_badargs() {
   let BAD_ID = 1000;
@@ -633,17 +645,17 @@ add_task(async function test_file_remova
 });
 
 add_task(async function test_removal_of_incomplete_download() {
   let url = getInterruptibleUrl();
   let msg = await runInExtension("download", {url});
   equal(msg.status, "success", "download() succeeded");
   const id = msg.result;
 
-  let progressPromise = waitForProgress(url, INT_PARTIAL_LEN);
+  let progressPromise = waitForProgress(url, bytes => bytes == INT_PARTIAL_LEN);
 
   msg = await runInExtension("waitForEvents", [
     {type: "onCreated", data: {id}},
   ]);
   equal(msg.status, "success", "got created and changed events");
 
   await progressPromise;
   do_print(`download reached ${INT_PARTIAL_LEN} bytes`);
@@ -852,11 +864,37 @@ add_task(async function test_getFileIcon
 
   msg = await runInExtension("getFileIcon", id, {size: 128});
   equal(msg.status, "error", "getFileIcon() fails");
   ok(msg.errmsg.includes("Error processing size"), "size is too big");
 
   webNav.close();
 });
 
+add_task(async function test_estimatedendtime() {
+  // Note we are not testing the actual value calculation of estimatedEndTime,
+  // only whether it is null/non-null at the appropriate times.
+
+  let url = `${getInterruptibleUrl()}&stream=1`;
+  let msg = await runInExtension("download", {url});
+  equal(msg.status, "success", "download() succeeded");
+  const id = msg.result;
+
+  let previousBytes = await waitForProgress(url, bytes => bytes > 0);
+  await waitForProgress(url, bytes => bytes > previousBytes);
+
+  msg = await runInExtension("search", {id});
+  equal(msg.status, "success", "search() succeeded");
+  equal(msg.result.length, 1, "search() found 1 download");
+  ok(msg.result[0].estimatedEndTime, "download.estimatedEndTime is correct");
+  ok(msg.result[0].bytesReceived > 0, "download.bytesReceived is correct");
+
+  msg = await runInExtension("cancel", id);
+
+  msg = await runInExtension("search", {id});
+  equal(msg.status, "success", "search() succeeded");
+  equal(msg.result.length, 1, "search() found 1 download");
+  ok(!msg.result[0].estimatedEndTime, "download.estimatedEndTime is correct");
+});
+
 add_task(async function cleanup() {
   await extension.unload();
 });
--- a/toolkit/components/jsoncpp/src/lib_json/moz.build
+++ b/toolkit/components/jsoncpp/src/lib_json/moz.build
@@ -39,11 +39,15 @@ if CONFIG['_MSC_VER']:
 elif CONFIG['GNU_CXX']:
     CXXFLAGS += [
         '-Wno-unused-local-typedefs',
         '-Wno-shadow',
     ]
 
 if CONFIG['CLANG_CXX'] or CONFIG['CLANG_CL']:
     CXXFLAGS += [
-        '-Wno-implicit-fallthrough',
         '-Wno-c++11-narrowing',
     ]
+
+if CONFIG['CLANG_CXX'] or CONFIG['CLANG_CL'] or CONFIG['GNU_CXX']:
+    CXXFLAGS += [
+        '-Wno-implicit-fallthrough',
+    ]
--- a/toolkit/content/aboutTelemetry.css
+++ b/toolkit/content/aboutTelemetry.css
@@ -5,34 +5,36 @@
 @import url("chrome://global/skin/in-content/common.css");
 
 html {
   height: 100%;
 }
 
 body {
   display: flex;
-  align-items: stretch;
   height: 100%;
 }
 
 #categories {
-  min-width: 250px;
   padding-top: 0px;
   overflow-y: auto;
 }
 
 .main-content.search > section > *:not(.data) {
   display: none;
 }
 
+.main-content {
+  flex: 1;
+  font-size: 18px;
+  line-height: 1.6;
+}
+
 #category-raw {
   background-color: var(--in-content-page-background);
-  box-sizing: border-box;
-  min-width: inherit;
   position: absolute;
   bottom: 0;
   left: 0;
 }
 
 .heading {
   display: flex;
   flex-direction: column;
@@ -111,24 +113,16 @@ body {
 .category.selected > .category-subsection {
   display: block;
 }
 
 .category-name {
   pointer-events: none;
 }
 
-.main-content {
-  width: 100%;
-  font-size: 18px;
-  line-height:1.6;
-  z-index: 1;
-  position: relative;
-}
-
 section:not(.active) {
   display: none;
 }
 
 #page-description {
   border: 1px solid threedshadow;
   margin: 0px;
   padding: 10px;
--- a/toolkit/crashreporter/crashreporter.mozbuild
+++ b/toolkit/crashreporter/crashreporter.mozbuild
@@ -25,14 +25,18 @@ elif CONFIG['GNU_CXX']:
         '-Wno-unused-local-typedefs',
         '-Wno-shadow',
         '-Wno-deprecated-declarations',
         '-Wno-bool-compare',
         '-Wno-unused-but-set-variable',
     ]
 
 if CONFIG['CLANG_CXX'] or CONFIG['CLANG_CL']:
-    CXXFLAGS += [
-        '-Wno-implicit-fallthrough',
-        '-Wno-c++11-narrowing',
-    ]
+     CXXFLAGS += [
+         '-Wno-c++11-narrowing',
+     ]
+
+if not CONFIG['_MSC_VER']:
+     CXXFLAGS += [
+         '-Wno-implicit-fallthrough',
+     ]
 
 DEFINES['NO_STABS_SUPPORT'] = True