Merge mozilla-central to autoland. a=merge on a CLOSED TREE
authorDaniel Varga <dvarga@mozilla.com>
Wed, 16 Jan 2019 06:56:15 +0200
changeset 511159 1c686d17264b9565aa3b67d2aca0034190a86f3a
parent 511158 932ddb2ffb5e1b5b94c63306ba59017ad08bf92c (current diff)
parent 511142 e4ac2508e8edc50da4bf7ed30803338a81ba026c (diff)
child 511160 cee164c51ba7d1d2d4c4395184fbc70eb45ba675
push id10547
push userffxbld-merge
push dateMon, 21 Jan 2019 13:03:58 +0000
treeherdermozilla-beta@24ec1916bffe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone66.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 autoland. a=merge on a CLOSED TREE
--- a/browser/components/aboutconfig/content/aboutconfig.css
+++ b/browser/components/aboutconfig/content/aboutconfig.css
@@ -58,17 +58,24 @@
   padding: 4px;
   width: 50%;
 }
 
 #prefs > tr > td.cell-name {
   padding-inline-start: 30px;
 }
 
+#prefs > tr.deleted > td.cell-name {
+  font-weight: bold;
+  opacity: 0.4;
+}
+
 .cell-value {
+  white-space: pre-wrap;
   word-break: break-all;
 }
 
-td.cell-value > form > input {
+td.cell-value > form > input[type="text"],
+td.cell-value > form > input[type="number"] {
   -moz-appearance: textfield;
   width: 100%;
   box-sizing: border-box;
 }
--- a/browser/components/aboutconfig/content/aboutconfig.html
+++ b/browser/components/aboutconfig/content/aboutconfig.html
@@ -30,14 +30,14 @@
 
       <div class="toggle-container-with-text">
         <input type="checkbox" id="showWarningNextTime" checked>
         <label for="showWarningNextTime"
                data-l10n-id="about-config-warning-checkbox"></label>
       </div>
 
       <div class="button-container">
-        <button class="primary" onclick="alterWarningState(); loadPrefs();"
+        <button class="primary" onclick="onWarningButtonClick();"
                 data-l10n-id="about-config-warning-button"></button>
       </div>
     </div>
   </body>
 </html>
--- a/browser/components/aboutconfig/content/aboutconfig.js
+++ b/browser/components/aboutconfig/content/aboutconfig.js
@@ -1,52 +1,98 @@
 /* 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/. */
 
+ChromeUtils.import("resource://gre/modules/DeferredTask.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/Preferences.jsm");
 
+const SEARCH_TIMEOUT_MS = 500;
+
 let gDefaultBranch = Services.prefs.getDefaultBranch("");
-let gPrefArray;
+let gFilterPrefsTask = new DeferredTask(() => filterPrefs(), SEARCH_TIMEOUT_MS);
+
+/**
+ * Maps the name of each preference in the back-end to its PrefRow object,
+ * separating the preferences that actually exist. This is as an optimization to
+ * avoid querying the preferences service each time the list is filtered.
+ */
+let gExistingPrefs = new Map();
+let gDeletedPrefs = new Map();
+
+/**
+ * Maps each row element currently in the table to its PrefRow object.
+ */
+let gElementToPrefMap = new WeakMap();
+
+/**
+ * Reference to the PrefRow currently being edited, if any.
+ */
 let gPrefInEdit = null;
 
+/**
+ * Lowercase substring that should be contained in the preference name.
+ */
+let gFilterString = null;
+
 class PrefRow {
   constructor(name) {
     this.name = name;
+    this.value = true;
     this.refreshValue();
 
     this.editing = false;
     this.element = document.createElement("tr");
-    this.element.setAttribute("aria-label", this.name);
     this._setupElement();
+    gElementToPrefMap.set(this.element, this);
   }
 
   refreshValue() {
+    this.hasDefaultValue = prefHasDefaultValue(this.name);
     this.hasUserValue = Services.prefs.prefHasUserValue(this.name);
-    this.hasDefaultValue = this.hasUserValue ? prefHasDefaultValue(this.name)
-                                             : true;
     this.isLocked = Services.prefs.prefIsLocked(this.name);
 
+    // If this preference has been deleted, we keep its last known value.
+    if (!this.exists) {
+      gExistingPrefs.delete(this.name);
+      gDeletedPrefs.set(this.name, this);
+      return;
+    }
+    gExistingPrefs.set(this.name, this);
+    gDeletedPrefs.delete(this.name);
+
     try {
       // This can throw for locked preferences without a default value.
       this.value = Preferences.get(this.name);
       // We don't know which preferences should be read using getComplexValue,
       // so we use a heuristic to determine if this is a localized preference.
       if (!this.hasUserValue &&
           /^chrome:\/\/.+\/locale\/.+\.properties/.test(this.value)) {
         // This can throw if there is no value in the localized files.
         this.value = Services.prefs.getComplexValue(this.name,
           Ci.nsIPrefLocalizedString).data;
       }
     } catch (ex) {
       this.value = "";
     }
   }
 
+  get type() {
+    return this.value.constructor.name;
+  }
+
+  get exists() {
+    return this.hasDefaultValue || this.hasUserValue;
+  }
+
+  get matchesFilter() {
+    return !gFilterString || this.name.toLowerCase().includes(gFilterString);
+  }
+
   _setupElement() {
     this.element.textContent = "";
     let nameCell = document.createElement("td");
     this.element.append(
       nameCell,
       this.valueCell = document.createElement("td"),
       this.editCell = document.createElement("td"),
       this.resetCell = document.createElement("td")
@@ -68,48 +114,86 @@ class PrefRow {
     nameCell.append(parts[parts.length - 1]);
 
     this.refreshElement();
   }
 
   refreshElement() {
     this.element.classList.toggle("has-user-value", !!this.hasUserValue);
     this.element.classList.toggle("locked", !!this.isLocked);
-    if (!this.editing) {
-      this.valueCell.textContent = this.value;
-      if (this.value.constructor.name == "Boolean") {
+    this.element.classList.toggle("deleted", !this.exists);
+    if (this.exists && !this.editing) {
+      // We need to place the text inside a "span" element to ensure that the
+      // text copied to the clipboard includes all whitespace.
+      let span = document.createElement("span");
+      span.textContent = this.value;
+      this.valueCell.textContent = "";
+      this.valueCell.append(span);
+      if (this.type == "Boolean") {
         document.l10n.setAttributes(this.editButton, "about-config-pref-toggle");
         this.editButton.className = "button-toggle";
       } else {
         document.l10n.setAttributes(this.editButton, "about-config-pref-edit");
         this.editButton.className = "button-edit";
       }
       this.editButton.removeAttribute("form");
       delete this.inputField;
     } else {
       this.valueCell.textContent = "";
       // The form is needed for the validation report to appear, but we need to
       // prevent the associated button from reloading the page.
       let form = document.createElement("form");
       form.addEventListener("submit", event => event.preventDefault());
       form.id = "form-edit";
-      this.inputField = document.createElement("input");
-      this.inputField.value = this.value;
-      if (this.value.constructor.name == "Number") {
-        this.inputField.type = "number";
-        this.inputField.required = true;
-        this.inputField.min = -2147483648;
-        this.inputField.max = 2147483647;
+      if (this.editing) {
+        this.inputField = document.createElement("input");
+        this.inputField.value = this.value;
+        if (this.type == "Number") {
+          this.inputField.type = "number";
+          this.inputField.required = true;
+          this.inputField.min = -2147483648;
+          this.inputField.max = 2147483647;
+        } else {
+          this.inputField.type = "text";
+        }
+        form.appendChild(this.inputField);
+        document.l10n.setAttributes(this.editButton, "about-config-pref-save");
+        this.editButton.className = "primary button-save";
       } else {
-        this.inputField.type = "text";
+        delete this.inputField;
+        for (let type of ["Boolean", "Number", "String"]) {
+          let radio = document.createElement("input");
+          radio.type = "radio";
+          radio.name = "type";
+          radio.value = type;
+          radio.checked = this.type == type;
+          form.appendChild(radio);
+          let radioLabel = document.createElement("span");
+          radioLabel.textContent = type;
+          form.appendChild(radioLabel);
+        }
+        form.addEventListener("click", event => {
+          if (event.target.name != "type") {
+            return;
+          }
+          let type = event.target.value;
+          if (this.type != type) {
+            if (type == "Boolean") {
+              this.value = true;
+            } else if (type == "Number") {
+              this.value = 0;
+            } else {
+              this.value = "";
+            }
+          }
+        });
+        document.l10n.setAttributes(this.editButton, "about-config-pref-add");
+        this.editButton.className = "button-add";
       }
-      form.appendChild(this.inputField);
       this.valueCell.appendChild(form);
-      document.l10n.setAttributes(this.editButton, "about-config-pref-save");
-      this.editButton.className = "primary button-save";
       this.editButton.setAttribute("form", "form-edit");
     }
     this.editButton.disabled = this.isLocked;
     if (!this.isLocked && this.hasUserValue) {
       if (!this.resetButton) {
         this.resetButton = document.createElement("button");
         this.resetCell.appendChild(this.resetButton);
       }
@@ -134,17 +218,17 @@ class PrefRow {
     }
     gPrefInEdit = this;
     this.editing = true;
     this.refreshElement();
     this.inputField.focus();
   }
 
   save() {
-    if (this.value.constructor.name == "Number") {
+    if (this.type == "Number") {
       if (!this.inputField.reportValidity()) {
         return;
       }
       Services.prefs.setIntPref(this.name, parseInt(this.inputField.value));
     } else {
       Services.prefs.setStringPref(this.name, this.inputField.value);
     }
     this.refreshValue();
@@ -154,148 +238,138 @@ class PrefRow {
 
   endEdit() {
     this.editing = false;
     this.refreshElement();
     gPrefInEdit = null;
   }
 }
 
-function getPrefName(prefRow) {
-  return prefRow.getAttribute("aria-label");
+let gPrefObserver = {
+  observe(subject, topic, data) {
+    let pref = gExistingPrefs.get(data) || gDeletedPrefs.get(data);
+    if (pref) {
+      pref.refreshValue();
+      if (!pref.editing) {
+        pref.refreshElement();
+      }
+      return;
+    }
+
+    let newPref = new PrefRow(data);
+    if (newPref.matchesFilter) {
+      document.getElementById("prefs").appendChild(newPref.element);
+    }
+  },
+};
+
+if (!Preferences.get("browser.aboutConfig.showWarning")) {
+  // When showing the filtered preferences directly, remove the warning elements
+  // immediately to prevent flickering, but wait to filter the preferences until
+  // the value of the textbox has been restored from previous sessions.
+  document.addEventListener("DOMContentLoaded", loadPrefs, { once: true });
+  window.addEventListener("load", filterPrefs, { once: true });
 }
 
-document.addEventListener("DOMContentLoaded", () => {
-  if (!Preferences.get("browser.aboutConfig.showWarning")) {
-    loadPrefs();
-  }
-}, { once: true });
-
-function alterWarningState() {
+function onWarningButtonClick() {
   Services.prefs.setBoolPref("browser.aboutConfig.showWarning",
     document.getElementById("showWarningNextTime").checked);
+  loadPrefs();
+  filterPrefs();
 }
 
 function loadPrefs() {
   [...document.styleSheets].find(s => s.title == "infop").disabled = true;
 
   document.body.textContent = "";
   let search = document.createElement("input");
   search.type = "text";
   search.id = "search";
   document.l10n.setAttributes(search, "about-config-search");
   document.body.appendChild(search);
   let prefs = document.createElement("table");
   prefs.id = "prefs";
   document.body.appendChild(prefs);
 
-  gPrefArray = Services.prefs.getChildList("").map(name => new PrefRow(name));
-
-  gPrefArray.sort((a, b) => a.name > b.name);
+  for (let name of Services.prefs.getChildList("")) {
+    new PrefRow(name);
+  }
 
-  search.addEventListener("keypress", e => {
-    if (e.key == "Enter") {
-      filterPrefs();
+  search.addEventListener("keypress", event => {
+    switch (event.key) {
+      case "Escape":
+        search.value = "";
+        // Fall through.
+      case "Enter":
+        gFilterPrefsTask.disarm();
+        filterPrefs();
     }
   });
 
+  search.addEventListener("input", () => {
+    // We call "disarm" to restart the timer at every input.
+    gFilterPrefsTask.disarm();
+    gFilterPrefsTask.arm();
+  });
+
   prefs.addEventListener("click", event => {
     if (event.target.localName != "button") {
       return;
     }
-    let prefRow = event.target.closest("tr");
-    let prefName = getPrefName(prefRow);
-    let pref = gPrefArray.find(p => p.name == prefName);
+    let pref = gElementToPrefMap.get(event.target.closest("tr"));
     let button = event.target.closest("button");
-    if (button.classList.contains("button-reset")) {
-      // Reset pref and update gPrefArray.
-      Services.prefs.clearUserPref(prefName);
-      pref.refreshValue();
-      pref.refreshElement();
-      pref.editButton.focus();
-    } else if (button.classList.contains("add-true")) {
-      addNewPref(prefRow.firstChild.innerHTML, true);
-    } else if (button.classList.contains("add-false")) {
-      addNewPref(prefRow.firstChild.innerHTML, false);
-    } else if (button.classList.contains("add-Number") ||
-      button.classList.contains("add-String")) {
-      addNewPref(prefRow.firstChild.innerHTML,
-        button.classList.contains("add-Number") ? 0 : "").edit();
+    if (button.classList.contains("button-add")) {
+      Preferences.set(pref.name, pref.value);
+      if (pref.type != "Boolean") {
+        pref.edit();
+      }
     } else if (button.classList.contains("button-toggle")) {
-      // Toggle the pref and update gPrefArray.
-      Services.prefs.setBoolPref(prefName, !pref.value);
-      pref.refreshValue();
-      pref.refreshElement();
+      Services.prefs.setBoolPref(pref.name, !pref.value);
     } else if (button.classList.contains("button-edit")) {
       pref.edit();
     } else if (button.classList.contains("button-save")) {
       pref.save();
     } else {
-      Services.prefs.clearUserPref(prefName);
-      gPrefArray.splice(gPrefArray.findIndex(p => p.name == prefName), 1);
-      prefRow.remove();
+      // This is "button-reset" or "button-delete".
+      pref.editing = false;
+      Services.prefs.clearUserPref(pref.name);
+      pref.editButton.focus();
     }
   });
 
-  filterPrefs();
+  Services.prefs.addObserver("", gPrefObserver);
+  window.addEventListener("unload", () => {
+    Services.prefs.removeObserver("", gPrefObserver);
+  }, { once: true });
 }
 
 function filterPrefs() {
   if (gPrefInEdit) {
     gPrefInEdit.endEdit();
   }
+  gDeletedPrefs.clear();
 
-  let substring = document.getElementById("search").value.trim();
-  document.getElementById("prefs").textContent = "";
-  if (substring && !gPrefArray.some(pref => pref.name == substring)) {
-    document.getElementById("prefs").appendChild(createNewPrefFragment(substring));
+  let searchName = document.getElementById("search").value.trim();
+  gFilterString = searchName.toLowerCase();
+  let prefArray = [...gExistingPrefs.values()];
+  if (gFilterString) {
+    prefArray = prefArray.filter(pref => pref.matchesFilter);
   }
-  let fragment = createPrefsFragment(gPrefArray.filter(pref => pref.name.includes(substring)));
-  document.getElementById("prefs").appendChild(fragment);
-}
+  prefArray.sort((a, b) => a.name > b.name);
+  if (searchName && !gExistingPrefs.has(searchName)) {
+    prefArray.push(new PrefRow(searchName));
+  }
 
-function createPrefsFragment(prefArray) {
+  let prefsElement = document.getElementById("prefs");
+  prefsElement.textContent = "";
   let fragment = document.createDocumentFragment();
   for (let pref of prefArray) {
     fragment.appendChild(pref.element);
   }
-  return fragment;
-}
-
-function createNewPrefFragment(name) {
-  let fragment = document.createDocumentFragment();
-  let row = document.createElement("tr");
-  row.classList.add("has-user-value");
-  row.setAttribute("aria-label", name);
-  let nameCell = document.createElement("td");
-  nameCell.className = "cell-name";
-  nameCell.append(name);
-  row.appendChild(nameCell);
-
-  let valueCell = document.createElement("td");
-  valueCell.classList.add("cell-value");
-  let guideText = document.createElement("span");
-  document.l10n.setAttributes(guideText, "about-config-pref-add");
-  valueCell.appendChild(guideText);
-  for (let item of ["true", "false", "Number", "String"]) {
-    let optionBtn = document.createElement("button");
-    optionBtn.textContent = item;
-    optionBtn.classList.add("add-" + item);
-    valueCell.appendChild(optionBtn);
-  }
-  row.appendChild(valueCell);
-
-  let editCell = document.createElement("td");
-  row.appendChild(editCell);
-
-  let buttonCell = document.createElement("td");
-  row.appendChild(buttonCell);
-
-  fragment.appendChild(row);
-  return fragment;
+  prefsElement.appendChild(fragment);
 }
 
 function prefHasDefaultValue(name) {
   try {
     switch (Services.prefs.getPrefType(name)) {
       case Ci.nsIPrefBranch.PREF_STRING:
         gDefaultBranch.getStringPref(name);
         return true;
@@ -304,17 +378,8 @@ function prefHasDefaultValue(name) {
         return true;
       case Ci.nsIPrefBranch.PREF_BOOL:
         gDefaultBranch.getBoolPref(name);
         return true;
     }
   } catch (ex) {}
   return false;
 }
-
-function addNewPref(name, value) {
-  Preferences.set(name, value);
-  let pref = new PrefRow(name);
-  gPrefArray.push(pref);
-  gPrefArray.sort((a, b) => a.name > b.name);
-  filterPrefs();
-  return pref;
-}
--- a/browser/components/aboutconfig/content/aboutconfig.notftl
+++ b/browser/components/aboutconfig/content/aboutconfig.notftl
@@ -8,14 +8,14 @@ about-config-warning-text = Changing the
 about-config-warning-checkbox = Annoy me again, please!
 about-config-warning-button = I accept the risk
 
 about-config-title = about:config
 
 about-config-search =
     .placeholder = Search
 
-about-config-pref-add = Add as:
+about-config-pref-add = Add
 about-config-pref-toggle = Toggle
 about-config-pref-edit = Edit
 about-config-pref-save = Save
 about-config-pref-reset = Reset
 about-config-pref-delete = Delete
--- a/browser/components/aboutconfig/test/browser/browser.ini
+++ b/browser/components/aboutconfig/test/browser/browser.ini
@@ -1,12 +1,16 @@
 [DEFAULT]
 support-files =
   head.js
 
 [browser_basic.js]
-[browser_warning.js]
+[browser_clipboard.js]
+skip-if = debug # Bug 1507747
+subsuite = clipboard
 [browser_edit.js]
 skip-if = debug # Bug 1507747
+[browser_locked.js]
+[browser_observe.js]
+skip-if = debug # Bug 1507747
 [browser_search.js]
 skip-if = debug # Bug 1507747
-[browser_locked.js]
-
+[browser_warning.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutconfig/test/browser/browser_clipboard.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function setup() {
+  await SpecialPowers.pushPrefEnv({
+    set: [
+      ["test.aboutconfig.copy.false", false],
+      ["test.aboutconfig.copy.number", 10],
+      ["test.aboutconfig.copy.spaces.1", " "],
+      ["test.aboutconfig.copy.spaces.2", "  "],
+      ["test.aboutconfig.copy.spaces.3", "   "],
+      ["test.aboutconfig.copy.string", "010.5"],
+    ],
+  });
+});
+
+add_task(async function test_copy() {
+  await AboutConfigTest.withNewTab(async function() {
+    for (let [name, expectedString] of [
+      [PREF_BOOLEAN_DEFAULT_TRUE, "true"],
+      [PREF_BOOLEAN_USERVALUE_TRUE, "true"],
+      [PREF_STRING_DEFAULT_EMPTY, ""],
+      ["test.aboutconfig.copy.false", "false"],
+      ["test.aboutconfig.copy.number", "10"],
+      ["test.aboutconfig.copy.spaces.1", " "],
+      ["test.aboutconfig.copy.spaces.2", "  "],
+      ["test.aboutconfig.copy.spaces.3", "   "],
+      ["test.aboutconfig.copy.string", "010.5"],
+    ]) {
+      // Limit the number of preferences shown so all the rows are visible.
+      this.search(name);
+      let row = this.getRow(name);
+
+      // Triple click at any location in the name cell should select the name.
+      await BrowserTestUtils.synthesizeMouseAtCenter(row.nameCell,
+                                                     { clickCount: 3 },
+                                                     this.browser);
+      Assert.ok(row.nameCell.contains(this.window.getSelection().anchorNode));
+      await SimpleTest.promiseClipboardChange(name, async () => {
+        await BrowserTestUtils.synthesizeKey("c", { accelKey: true },
+                                             this.browser);
+      });
+
+      // Triple click at any location in the value cell should select the value.
+      await BrowserTestUtils.synthesizeMouseAtCenter(row.valueCell,
+                                                     { clickCount: 3 },
+                                                     this.browser);
+      let selection = this.window.getSelection();
+      Assert.ok(row.valueCell.contains(selection.anchorNode));
+
+      // The selection is never collapsed because of the <span> element, and
+      // this makes sure that an empty string can be copied.
+      Assert.ok(!selection.isCollapsed);
+      await SimpleTest.promiseClipboardChange(expectedString, async () => {
+        await BrowserTestUtils.synthesizeKey("c", { accelKey: true },
+                                             this.browser);
+      });
+    }
+  });
+});
+
+add_task(async function test_copy_multiple() {
+  await AboutConfigTest.withNewTab(async function() {
+    // Lines are separated by a single LF character on all platforms.
+    let expectedString = "test.aboutconfig.copy.false\tfalse\t\n" +
+                         "test.aboutconfig.copy.number\t10\t\n" +
+                         "test.aboutconfig.copy.spaces.1\t \t\n" +
+                         "test.aboutconfig.copy.spaces.2\t  \t\n" +
+                         "test.aboutconfig.copy.spaces.3\t   \t\n" +
+                         "test.aboutconfig.copy.string\t010.5";
+
+    this.search("test.aboutconfig.copy.");
+    let startRow = this.getRow("test.aboutconfig.copy.false");
+    let endRow = this.getRow("test.aboutconfig.copy.string");
+    let { width, height } = endRow.valueCell.getBoundingClientRect();
+
+    // Drag from the top left of the first row to the bottom right of the last.
+    await BrowserTestUtils.synthesizeMouse(startRow.nameCell, 1, 1,
+                                           { type: "mousedown" }, this.browser);
+    await BrowserTestUtils.synthesizeMouse(endRow.valueCell,
+                                           width - 1, height - 1,
+                                           { type: "mousemove" }, this.browser);
+    await BrowserTestUtils.synthesizeMouse(endRow.valueCell,
+                                           width - 1, height - 1,
+                                           { type: "mouseup" }, this.browser);
+
+    await SimpleTest.promiseClipboardChange(expectedString, async () => {
+      await BrowserTestUtils.synthesizeKey("c", { accelKey: true },
+                                           this.browser);
+    });
+  });
+});
--- a/browser/components/aboutconfig/test/browser/browser_edit.js
+++ b/browser/components/aboutconfig/test/browser/browser_edit.js
@@ -13,46 +13,81 @@ add_task(async function setup() {
   registerCleanupFunction(() => {
     Services.prefs.clearUserPref(PREF_BOOLEAN_DEFAULT_TRUE);
     Services.prefs.clearUserPref(PREF_NUMBER_DEFAULT_ZERO);
     Services.prefs.clearUserPref(PREF_STRING_DEFAULT_EMPTY);
   });
 });
 
 add_task(async function test_add_user_pref() {
+  Assert.equal(Services.prefs.getPrefType(PREF_NEW),
+               Ci.nsIPrefBranch.PREF_INVALID);
+
   await AboutConfigTest.withNewTab(async function() {
-    Assert.ok(!Services.prefs.getChildList("").find(pref => pref == "testPref"));
+    // The row for a new preference appears when searching for its name.
+    Assert.ok(!this.getRow(PREF_NEW));
+    this.search(PREF_NEW);
+    let row = this.getRow(PREF_NEW);
+    Assert.ok(row.hasClass("deleted"));
 
-    for (let [buttonSelector, expectedValue] of [
-      [".add-true", true],
-      [".add-false", false],
-      [".add-Number", 0],
-      [".add-String", ""],
+    for (let [radioIndex, expectedValue, expectedEditingMode] of [
+      [0, true, false],
+      [1, 0, true],
+      [2, "", true],
     ]) {
-      this.search("testPref");
-      this.document.querySelector("#prefs button" + buttonSelector).click();
-      Assert.ok(Services.prefs.getChildList("").find(pref => pref == "testPref"));
-      Assert.ok(Preferences.get("testPref") === expectedValue);
-      this.document.querySelector("#prefs button[data-l10n-id='about-config-pref-delete']").click();
+      // Adding the preference should set the default for the data type.
+      row.element.querySelectorAll("input")[radioIndex].click();
+      row.editColumnButton.click();
+      Assert.ok(!row.hasClass("deleted"));
+      Assert.ok(Preferences.get(PREF_NEW) === expectedValue);
+
+      // Number and String preferences should be in edit mode.
+      Assert.equal(!!row.valueInput, expectedEditingMode);
+
+      // Repeat the search to verify that the preference remains.
+      this.search(PREF_NEW);
+      row = this.getRow(PREF_NEW);
+      Assert.ok(!row.hasClass("deleted"));
+      Assert.ok(!row.valueInput);
+
+      // Reset the preference, then continue by adding a different type.
+      row.resetColumnButton.click();
+      Assert.equal(Services.prefs.getPrefType(PREF_NEW),
+                   Ci.nsIPrefBranch.PREF_INVALID);
     }
   });
 });
 
 add_task(async function test_delete_user_pref() {
-  Services.prefs.setBoolPref("userAddedPref", true);
-  await AboutConfigTest.withNewTab(async function() {
-    let row = this.getRow("userAddedPref");
-    row.resetColumnButton.click();
-    Assert.ok(!this.getRow("userAddedPref"));
-    Assert.ok(!Services.prefs.getChildList("").includes("userAddedPref"));
+  for (let [radioIndex, testValue] of [
+    [0, false],
+    [1, -1],
+    [2, "value"],
+  ]) {
+    Preferences.set(PREF_NEW, testValue);
+    await AboutConfigTest.withNewTab(async function() {
+      // Deleting the preference should keep the row.
+      let row = this.getRow(PREF_NEW);
+      row.resetColumnButton.click();
+      Assert.ok(row.hasClass("deleted"));
+      Assert.equal(Services.prefs.getPrefType(PREF_NEW),
+                   Ci.nsIPrefBranch.PREF_INVALID);
 
-    // Search for nothing to test gPrefArray
-    this.search();
-    Assert.ok(!this.getRow("userAddedPref"));
-  });
+      // Re-adding the preference should keep the same value.
+      Assert.ok(row.element.querySelectorAll("input")[radioIndex].checked);
+      row.editColumnButton.click();
+      Assert.ok(!row.hasClass("deleted"));
+      Assert.ok(Preferences.get(PREF_NEW) === testValue);
+
+      // Searching again after deleting should remove the row.
+      row.resetColumnButton.click();
+      this.search();
+      Assert.ok(!this.getRow(PREF_NEW));
+    });
+  }
 });
 
 add_task(async function test_reset_user_pref() {
   await SpecialPowers.pushPrefEnv({
     "set": [
       [PREF_BOOLEAN_DEFAULT_TRUE, false],
       [PREF_STRING_LOCALIZED_MISSING, "user-value"],
     ],
@@ -143,16 +178,13 @@ add_task(async function test_modify() {
       row.editColumnButton.click();
       Assert.equal(Preferences.get(prefName), "42");
       Assert.equal(row.value, "42");
       Assert.ok(row.hasClass("has-user-value"));
       // Reset or delete the preference while editing.
       row.editColumnButton.click();
       Assert.equal(row.valueInput.value, Preferences.get(prefName));
       row.resetColumnButton.click();
-      if (willDelete) {
-        Assert.ok(!this.getRow(prefName));
-      } else {
-        Assert.ok(!row.hasClass("has-user-value"));
-      }
+      Assert.ok(!row.hasClass("has-user-value"));
+      Assert.equal(row.hasClass("deleted"), willDelete);
     }
   });
 });
new file mode 100644
--- /dev/null
+++ b/browser/components/aboutconfig/test/browser/browser_observe.js
@@ -0,0 +1,140 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function setup() {
+  await SpecialPowers.pushPrefEnv({
+    set: [
+      ["test.aboutconfig.modify.boolean", true],
+      ["test.aboutconfig.modify.number", 1337],
+      ["test.aboutconfig.modify.string", "the answer to the life the universe and everything"],
+    ],
+  });
+
+  registerCleanupFunction(() => {
+    Services.prefs.clearUserPref(PREF_BOOLEAN_DEFAULT_TRUE);
+    Services.prefs.clearUserPref(PREF_NUMBER_DEFAULT_ZERO);
+    Services.prefs.clearUserPref(PREF_STRING_DEFAULT_EMPTY);
+  });
+});
+
+add_task(async function test_observe_add_user_pref() {
+  Assert.equal(Services.prefs.getPrefType(PREF_NEW),
+               Ci.nsIPrefBranch.PREF_INVALID);
+
+  await AboutConfigTest.withNewTab(async function() {
+    for (let value of [false, true, "", "value", 0, -10]) {
+      // A row should be added when a new preference is added.
+      Assert.ok(!this.getRow(PREF_NEW));
+      Preferences.set(PREF_NEW, value);
+      let row = this.getRow(PREF_NEW);
+      Assert.equal(row.value, "" + value);
+
+      // The row should stay when the preference is removed.
+      Preferences.reset(PREF_NEW);
+      Assert.ok(row.hasClass("deleted"));
+
+      // Re-adding the preference from the interface should restore its value.
+      row.editColumnButton.click();
+      if (value.constructor.name != "Boolean") {
+        row.editColumnButton.click();
+      }
+      Assert.equal(row.value, "" + value);
+      Assert.ok(Preferences.get(PREF_NEW) === value);
+
+      // Searching again after deleting should remove the row.
+      Preferences.reset(PREF_NEW);
+      this.search();
+      Assert.ok(!this.getRow(PREF_NEW));
+
+      // Searching for the preference name should give the ability to add it.
+      Preferences.reset(PREF_NEW);
+      this.search(PREF_NEW);
+      row = this.getRow(PREF_NEW);
+      Assert.ok(row.hasClass("deleted"));
+
+      // The row for adding should be reused if the new preference is added.
+      Preferences.set(PREF_NEW, value);
+      Assert.equal(row.value, "" + value);
+
+      // If a new preference does not match the filter it is not displayed.
+      Preferences.reset(PREF_NEW);
+      this.search(PREF_NEW + ".extra");
+      Assert.ok(!this.getRow(PREF_NEW));
+      Preferences.set(PREF_NEW, value);
+      Assert.ok(!this.getRow(PREF_NEW));
+
+      // Resetting the filter should display the new preference.
+      this.search("");
+      Assert.equal(this.getRow(PREF_NEW).value, "" + value);
+
+      // Reset the preference, then continue by adding a different value.
+      Preferences.reset(PREF_NEW);
+      this.search("");
+    }
+  });
+});
+
+add_task(async function test_observe_delete_user_pref() {
+  for (let value of [true, "value", -10]) {
+    Preferences.set(PREF_NEW, value);
+    await AboutConfigTest.withNewTab(async function() {
+      // Deleting the preference should keep the row.
+      let row = this.getRow(PREF_NEW);
+      Preferences.reset(PREF_NEW);
+      Assert.ok(row.hasClass("deleted"));
+
+      // Searching again should remove the row.
+      this.search();
+      Assert.ok(!this.getRow(PREF_NEW));
+    });
+  }
+});
+
+add_task(async function test_observe_reset_user_pref() {
+  await SpecialPowers.pushPrefEnv({
+    "set": [
+      [PREF_BOOLEAN_DEFAULT_TRUE, false],
+    ],
+  });
+
+  await AboutConfigTest.withNewTab(async function() {
+    let row = this.getRow(PREF_BOOLEAN_DEFAULT_TRUE);
+    Preferences.reset(PREF_BOOLEAN_DEFAULT_TRUE);
+    Assert.ok(!row.hasClass("has-user-value"));
+    Assert.equal(row.value, "true");
+  });
+});
+
+add_task(async function test_observe_modify() {
+  await AboutConfigTest.withNewTab(async function() {
+    for (let [name, value] of [
+      ["test.aboutconfig.modify.boolean", false],
+      ["test.aboutconfig.modify.number", -10],
+      ["test.aboutconfig.modify.string", "value"],
+      [PREF_BOOLEAN_DEFAULT_TRUE, false],
+      [PREF_NUMBER_DEFAULT_ZERO, 1],
+      [PREF_STRING_DEFAULT_EMPTY, "string"],
+    ]) {
+      let row = this.getRow(name);
+      Assert.notEqual(row.value, "" + value);
+      Preferences.set(name, value);
+      Assert.equal(row.value, "" + value);
+
+      if (value.constructor.name == "Boolean") {
+        continue;
+      }
+
+      // Changing the value or removing while editing should not take effect.
+      row.editColumnButton.click();
+      row.valueInput.value = "42";
+      Preferences.reset(name);
+      Assert.equal(row.element, this.getRow(name).element);
+      Assert.equal(row.valueInput.value, "42");
+
+      // Saving should store the value even if the preference was modified.
+      row.editColumnButton.click();
+      Assert.equal(row.value, "42");
+      Assert.equal(Preferences.get(name), "42");
+    }
+  });
+});
--- a/browser/components/aboutconfig/test/browser/browser_search.js
+++ b/browser/components/aboutconfig/test/browser/browser_search.js
@@ -20,17 +20,17 @@ add_task(async function test_search() {
     // The change in count would be because of one or two added preferences,
     // but we tolerate a difference of up to 50 preferences just to be safe.
     // We want thousands of prefs instead of a few dozen that are filtered.
     Assert.greater(this.rows.length, prefArray.length - 50);
 
     // Filter a subset of preferences. The "browser.download." branch is
     // chosen because it is very unlikely that its preferences would be
     // modified by other code during the execution of this test.
-    this.search("wser.down   ");
+    this.search("Wser.down   ");
 
     let filteredPrefArray =
         prefArray.filter(pref => pref.includes("wser.down"));
     // Adding +1 to the list since button does not match an exact
     // preference name then a row is added for the user to add a
     // new button preference if desired
     Assert.equal(this.rows.length, filteredPrefArray.length + 1);
 
@@ -44,13 +44,46 @@ add_task(async function test_search() {
     // We want thousands of prefs instead of a few dozen that are filtered.
     Assert.greater(this.rows.length, prefArray.length - 50);
 
     // Test invalid search returns no preferences.
     // Expecting 1 row to be returned since it offers the ability to add.
     this.search("aJunkValueasdf");
     Assert.equal(this.rows.length, 1);
 
-    // Test added preferences search returns 2 preferences.
+    // Two preferences match this filter, and one of those matches exactly.
+    this.search("test.aboutconfig.a");
+    Assert.equal(this.rows.length, 2);
+
+    // When searching case insensitively, there is an additional row to add a
+    // new preference with the same name but a different case.
+    this.search("TEST.aboutconfig.a");
+    Assert.equal(this.rows.length, 3);
+  });
+});
+
+add_task(async function test_search_delayed() {
+  await AboutConfigTest.withNewTab(async function() {
+    let prefs = this.document.getElementById("prefs");
+
+    // Prepare the table and the search field for the test.
     this.search("test.aboutconfig.a");
     Assert.equal(this.rows.length, 2);
+
+    // The table is updated in a single microtask, so we don't need to wait for
+    // specific mutations, we can just continue when the children are updated.
+    let prefsTableChanged = new Promise(resolve => {
+      let observer = new MutationObserver(() => {
+        observer.disconnect();
+        resolve();
+      });
+      observer.observe(prefs, { childList: true });
+    });
+
+    // Add a character and test that the table is not updated immediately.
+    EventUtils.synthesizeKey("b");
+    Assert.equal(this.rows.length, 2);
+
+    // The table will eventually be updated after a delay.
+    await prefsTableChanged;
+    Assert.equal(this.rows.length, 1);
   });
 });
--- a/browser/components/aboutconfig/test/browser/head.js
+++ b/browser/components/aboutconfig/test/browser/head.js
@@ -12,43 +12,54 @@ ChromeUtils.import("resource://gre/modul
 const PREF_BOOLEAN_DEFAULT_TRUE = "accessibility.typeaheadfind.manual";
 const PREF_BOOLEAN_USERVALUE_TRUE = "browser.dom.window.dump.enabled";
 const PREF_NUMBER_DEFAULT_ZERO = "accessibility.typeaheadfind.casesensitive";
 const PREF_STRING_DEFAULT_EMPTY = "browser.helperApps.neverAsk.openFile";
 const PREF_STRING_DEFAULT_NOTEMPTY = "accessibility.typeaheadfind.soundURL";
 const PREF_STRING_DEFAULT_NOTEMPTY_VALUE = "beep";
 const PREF_STRING_LOCALIZED_MISSING = "gecko.handlerService.schemes.irc.1.name";
 
+// Other preference names used in tests.
+const PREF_NEW = "test.aboutconfig.new";
+
 class AboutConfigRowTest {
   constructor(element) {
     this.element = element;
   }
 
   querySelector(selector) {
     return this.element.querySelector(selector);
   }
 
+  get nameCell() {
+    return this.querySelector("td");
+  }
+
   get name() {
-    return this.querySelector("td").textContent;
+    return this.nameCell.textContent;
+  }
+
+  get valueCell() {
+    return this.querySelector("td.cell-value");
   }
 
   get value() {
-    return this.querySelector("td.cell-value").textContent;
+    return this.valueCell.textContent;
   }
 
   /**
    * Text input field when the row is in edit mode.
    */
   get valueInput() {
-    return this.querySelector("td.cell-value input");
+    return this.valueCell.querySelector("input");
   }
 
   /**
-   * This is normally "edit" or "toggle" based on the preference type, or "save"
-   * when the row is in edit mode.
+   * This is normally "edit" or "toggle" based on the preference type, "save"
+   * when the row is in edit mode, or "add" when the preference does not exist.
    */
   get editColumnButton() {
     return this.querySelector("td.cell-edit > button");
   }
 
   /**
    * This can be "reset" or "delete" based on whether a default exists.
    */
@@ -69,17 +80,19 @@ class AboutConfigTest {
     }, async browser => {
       let scope = new this(browser);
       await scope.setupNewTab(options);
       await testFn.call(scope);
     });
   }
 
   constructor(browser) {
+    this.browser = browser;
     this.document = browser.contentDocument;
+    this.window = browser.contentWindow;
   }
 
   async setupNewTab(options) {
     await this.document.l10n.ready;
     if (!options.dontBypassWarning) {
       this.document.querySelector("button").click();
     }
   }
--- a/browser/components/preferences/in-content/tests/browser.ini
+++ b/browser/components/preferences/in-content/tests/browser.ini
@@ -49,17 +49,17 @@ skip-if = (verify && debug && (os == 'li
 [browser_contentblocking.js]
 [browser_cookies_exceptions.js]
 [browser_defaultbrowser_alwayscheck.js]
 [browser_healthreport.js]
 skip-if = true || !healthreport # Bug 1185403 for the "true"
 [browser_homepages_filter_aboutpreferences.js]
 [browser_homepages_use_bookmark.js]
 [browser_extension_controlled.js]
-skipif = ccov && os == 'win' # bug 1437051
+skip-if = ccov && os == 'win' # bug 1437051
 [browser_languages_subdialog.js]
 [browser_browser_languages_subdialog.js]
 [browser_layersacceleration.js]
 [browser_masterpassword.js]
 [browser_newtab_menu.js]
 [browser_notifications_do_not_disturb.js]
 [browser_password_management.js]
 [browser_performance.js]
--- a/dom/base/nsDOMNavigationTiming.cpp
+++ b/dom/base/nsDOMNavigationTiming.cpp
@@ -20,16 +20,24 @@
 #include "mozilla/TimeStamp.h"
 #include "mozilla/Telemetry.h"
 #ifdef MOZ_GECKO_PROFILER
 #include "ProfilerMarkerPayload.h"
 #endif
 
 using namespace mozilla;
 
+namespace mozilla {
+
+LazyLogModule gPageLoadLog("PageLoad");
+#define PAGELOAD_LOG(args) MOZ_LOG(gPageLoadLog, LogLevel::Debug, args)
+#define PAGELOAD_LOG_ENABLED() MOZ_LOG_TEST(gPageLoadLog, LogLevel::Error)
+
+}  // namespace mozilla
+
 nsDOMNavigationTiming::nsDOMNavigationTiming(nsDocShell* aDocShell) {
   Clear();
 
   mDocShell = aDocShell;
 }
 
 nsDOMNavigationTiming::~nsDOMNavigationTiming() {}
 
@@ -135,16 +143,35 @@ void nsDOMNavigationTiming::NotifyLoadEv
     return;
   }
   mLoadEventEnd = TimeStamp::Now();
 
   PROFILER_TRACING_DOCSHELL("Navigation", "Load", TRACING_INTERVAL_END,
                             mDocShell);
 
   if (IsTopLevelContentDocumentInContentProcess()) {
+#ifdef MOZ_GECKO_PROFILER
+    if (profiler_is_active() || PAGELOAD_LOG_ENABLED()) {
+      TimeDuration elapsed = mLoadEventEnd - mNavigationStart;
+      TimeDuration duration = mLoadEventEnd - mLoadEventStart;
+      nsAutoCString spec;
+      if (mLoadedURI) {
+        mLoadedURI->GetSpec(spec);
+      }
+      nsPrintfCString marker(
+          "Document %s loaded after %dms, load event duration %dms", spec.get(),
+          int(elapsed.ToMilliseconds()), int(duration.ToMilliseconds()));
+      DECLARE_DOCSHELL_AND_HISTORY_ID(mDocShell);
+      PAGELOAD_LOG(("%s", marker.get()));
+      profiler_add_marker(
+          "DocumentLoad",
+          MakeUnique<TextMarkerPayload>(marker, mNavigationStart, mLoadEventEnd,
+                                        docShellId, docShellHistoryId));
+    }
+#endif
     Telemetry::AccumulateTimeDelta(Telemetry::TIME_TO_LOAD_EVENT_END_MS,
                                    mNavigationStart);
   }
 }
 
 void nsDOMNavigationTiming::SetDOMLoadingTimeStamp(nsIURI* aURI,
                                                    TimeStamp aValue) {
   if (!mDOMLoading.IsNull()) {
@@ -235,142 +262,143 @@ void nsDOMNavigationTiming::NotifyDOMCon
 
 // static
 void nsDOMNavigationTiming::TTITimeoutCallback(nsITimer* aTimer,
                                                void* aClosure) {
   nsDOMNavigationTiming* self = static_cast<nsDOMNavigationTiming*>(aClosure);
   self->TTITimeout(aTimer);
 }
 
-// Return the max of aT1 and aT2, or the lower of the two if there's more
-// than Nms (the window size) between them.  In other words, the window
-// starts at the lower of aT1 and aT2, and we only want to respect
-// timestamps within the window (and pick the max of those).
-//
-// This approach handles the edge case of a late wakeup: where there was
-// more than Nms after one (of aT1 or aT2) without the other, but the other
-// happened after Nms and before we woke up.  For example, if aT1 was 10
-// seconds after aT2, but we woke up late (after aT1) we don't want to
-// return aT1 if the window is 5 seconds.
-static const TimeStamp& MaxWithinWindowBeginningAtMin(
-    const TimeStamp& aT1, const TimeStamp& aT2,
-    const TimeDuration& aWindowSize) {
-  if (aT2.IsNull()) {
-    return aT1;
-  } else if (aT1.IsNull()) {
-    return aT2;
-  }
-  if (aT1 > aT2) {
-    if ((aT1 - aT2) > aWindowSize) {
-      return aT2;
-    }
-    return aT1;
-  }
-  if ((aT2 - aT1) > aWindowSize) {
-    return aT1;
-  }
-  return aT2;
-}
-
 #define TTI_WINDOW_SIZE_MS (5 * 1000)
 
 void nsDOMNavigationTiming::TTITimeout(nsITimer* aTimer) {
   // Check TTI: see if it's been 5 seconds since the last Long Task
   TimeStamp now = TimeStamp::Now();
   MOZ_RELEASE_ASSERT(!mContentfulPaint.IsNull(),
                      "TTI timeout with no contentful-paint?");
 
   nsCOMPtr<nsIThread> mainThread = do_GetMainThread();
   TimeStamp lastLongTaskEnded;
   mainThread->GetLastLongNonIdleTaskEnd(&lastLongTaskEnded);
-  if (!lastLongTaskEnded.IsNull()) {
-    TimeDuration delta = now - lastLongTaskEnded;
-    if (delta.ToMilliseconds() < TTI_WINDOW_SIZE_MS) {
-      // Less than 5 seconds since the last long task.  Schedule another check
-      aTimer->InitWithNamedFuncCallback(TTITimeoutCallback, this,
-                                        TTI_WINDOW_SIZE_MS,
-                                        nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY,
-                                        "nsDOMNavigationTiming::TTITimeout");
-      return;
-    }
+  // Window starts at mContentfulPaint; any long task before that is ignored
+  if (lastLongTaskEnded.IsNull() || lastLongTaskEnded < mContentfulPaint) {
+    PAGELOAD_LOG(
+        ("no longtask (last was %g ms before ContentfulPaint)",
+         lastLongTaskEnded.IsNull()
+             ? 0
+             : (mContentfulPaint - lastLongTaskEnded).ToMilliseconds()));
+    lastLongTaskEnded = mContentfulPaint;
   }
+  TimeDuration delta = now - lastLongTaskEnded;
+  PAGELOAD_LOG(("TTI delta: %g ms", delta.ToMilliseconds()));
+  if (delta.ToMilliseconds() < TTI_WINDOW_SIZE_MS) {
+    // Less than 5 seconds since the last long task or start of the window.
+    // Schedule another check.
+    PAGELOAD_LOG(("TTI: waiting additional %g ms",
+                  (TTI_WINDOW_SIZE_MS + 100) - delta.ToMilliseconds()));
+    aTimer->InitWithNamedFuncCallback(
+        TTITimeoutCallback, this,
+        (TTI_WINDOW_SIZE_MS + 100) -
+            delta.ToMilliseconds(),  // slightly after the window ends
+        nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY,
+        "nsDOMNavigationTiming::TTITimeout");
+    return;
+  }
+
   // To correctly implement TTI/TTFI as proposed, we'd need to not
   // fire it until there are no more than 2 network loads.  By the
   // proposed definition, without that we're closer to
-  // TimeToFirstInteractive.
+  // TimeToFirstInteractive.  There are also arguments about what sort
+  // of loads should qualify.
 
   // XXX check number of network loads, and if > 2 mark to check if loads
   // decreases to 2 (or record that point and let the normal timer here
   // handle it)
 
   // TTI has occurred!  TTI is either FCP (if there are no longtasks and no
   // DCLEnd in the window that starts at FCP), or at the end of the last
-  // Long Task or DOMContentLoadedEnd (whichever is later).
+  // Long Task or DOMContentLoadedEnd (whichever is later). lastLongTaskEnded
+  // is >= FCP here.
 
   if (mTTFI.IsNull()) {
-    mTTFI = MaxWithinWindowBeginningAtMin(
-        lastLongTaskEnded, mDOMContentLoadedEventEnd,
-        TimeDuration::FromMilliseconds(TTI_WINDOW_SIZE_MS));
-    if (mTTFI.IsNull()) {
-      mTTFI = mContentfulPaint;
-    }
+    // lastLongTaskEnded is >= mContentfulPaint
+    mTTFI = (mDOMContentLoadedEventEnd.IsNull() ||
+             lastLongTaskEnded > mDOMContentLoadedEventEnd)
+                ? lastLongTaskEnded
+                : mDOMContentLoadedEventEnd;
+    PAGELOAD_LOG(
+        ("TTFI after %dms (LongTask was at %dms, DCL was %dms)",
+         int((mTTFI - mNavigationStart).ToMilliseconds()),
+         lastLongTaskEnded.IsNull()
+             ? 0
+             : int((lastLongTaskEnded - mNavigationStart).ToMilliseconds()),
+         mDOMContentLoadedEventEnd.IsNull()
+             ? 0
+             : int((mDOMContentLoadedEventEnd - mNavigationStart)
+                       .ToMilliseconds())));
   }
   // XXX Implement TTI via check number of network loads, and if > 2 mark
   // to check if loads decreases to 2 (or record that point and let the
   // normal timer here handle it)
 
   mTTITimer = nullptr;
 
 #ifdef MOZ_GECKO_PROFILER
-  if (profiler_is_active()) {
+  if (profiler_is_active() || PAGELOAD_LOG_ENABLED()) {
     TimeDuration elapsed = mTTFI - mNavigationStart;
+    MOZ_ASSERT(elapsed.ToMilliseconds() > 0);
     TimeDuration elapsedLongTask =
         lastLongTaskEnded.IsNull() ? 0 : lastLongTaskEnded - mNavigationStart;
     nsAutoCString spec;
     if (mLoadedURI) {
       mLoadedURI->GetSpec(spec);
     }
-    nsPrintfCString marker("TTFI after %dms (LongTask after %dms) for URL %s",
+    nsPrintfCString marker("TTFI after %dms (LongTask was at %dms) for URL %s",
                            int(elapsed.ToMilliseconds()),
                            int(elapsedLongTask.ToMilliseconds()), spec.get());
 
     DECLARE_DOCSHELL_AND_HISTORY_ID(mDocShell);
-    profiler_add_marker("TTI", MakeUnique<UserTimingMarkerPayload>(
-                                   NS_ConvertASCIItoUTF16(marker), mTTFI,
-                                   docShellId, docShellHistoryId));
+    profiler_add_marker(
+        "TTFI", MakeUnique<TextMarkerPayload>(marker, mNavigationStart, mTTFI,
+                                              docShellId, docShellHistoryId));
   }
 #endif
   return;
 }
 
 void nsDOMNavigationTiming::NotifyNonBlankPaintForRootContentDocument() {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(!mNavigationStart.IsNull());
 
   if (!mNonBlankPaint.IsNull()) {
     return;
   }
 
   mNonBlankPaint = TimeStamp::Now();
 
 #ifdef MOZ_GECKO_PROFILER
-  if (profiler_thread_is_being_profiled()) {
+  if (profiler_thread_is_being_profiled() || PAGELOAD_LOG_ENABLED()) {
     TimeDuration elapsed = mNonBlankPaint - mNavigationStart;
     nsAutoCString spec;
     if (mLoadedURI) {
       mLoadedURI->GetSpec(spec);
     }
     nsPrintfCString marker(
         "Non-blank paint after %dms for URL %s, %s",
         int(elapsed.ToMilliseconds()), spec.get(),
         mDocShellHasBeenActiveSinceNavigationStart
             ? "foreground tab"
             : "this tab was inactive some of the time between navigation start "
               "and first non-blank paint");
-    profiler_add_marker(marker.get());
+    PAGELOAD_LOG(("%s", marker.get()));
+    DECLARE_DOCSHELL_AND_HISTORY_ID(mDocShell);
+    profiler_add_marker(
+        "FirstNonBlankPaint",
+        MakeUnique<TextMarkerPayload>(marker, mNavigationStart, mNonBlankPaint,
+                                      docShellId, docShellHistoryId));
   }
 #endif
 
   if (mDocShellHasBeenActiveSinceNavigationStart) {
     if (net::nsHttp::IsBeforeLastActiveTabLoadOptimization(mNavigationStart)) {
       Telemetry::AccumulateTimeDelta(
           Telemetry::TIME_TO_NON_BLANK_PAINT_NETOPT_MS, mNavigationStart,
           mNonBlankPaint);
@@ -391,30 +419,35 @@ void nsDOMNavigationTiming::NotifyConten
 
   if (!mContentfulPaint.IsNull()) {
     return;
   }
 
   mContentfulPaint = TimeStamp::Now();
 
 #ifdef MOZ_GECKO_PROFILER
-  if (profiler_is_active()) {
+  if (profiler_is_active() || PAGELOAD_LOG_ENABLED()) {
     TimeDuration elapsed = mContentfulPaint - mNavigationStart;
     nsAutoCString spec;
     if (mLoadedURI) {
       mLoadedURI->GetSpec(spec);
     }
     nsPrintfCString marker(
         "Contentful paint after %dms for URL %s, %s",
         int(elapsed.ToMilliseconds()), spec.get(),
         mDocShellHasBeenActiveSinceNavigationStart
             ? "foreground tab"
             : "this tab was inactive some of the time between navigation start "
               "and first non-blank paint");
-    profiler_add_marker(marker.get());
+    DECLARE_DOCSHELL_AND_HISTORY_ID(mDocShell);
+    PAGELOAD_LOG(("%s", marker.get()));
+    profiler_add_marker("FirstContentfulPaint",
+                        MakeUnique<TextMarkerPayload>(
+                            marker, mNavigationStart, mContentfulPaint,
+                            docShellId, docShellHistoryId));
   }
 #endif
 
   if (!mTTITimer) {
     mTTITimer = NS_NewTimer();
   }
 
   // TTI is first checked 5 seconds after the FCP (non-blank-paint is very close
@@ -431,30 +464,35 @@ void nsDOMNavigationTiming::NotifyDOMCon
 
   if (!mDOMContentFlushed.IsNull()) {
     return;
   }
 
   mDOMContentFlushed = TimeStamp::Now();
 
 #ifdef MOZ_GECKO_PROFILER
-  if (profiler_thread_is_being_profiled()) {
+  if (profiler_thread_is_being_profiled() || PAGELOAD_LOG_ENABLED()) {
     TimeDuration elapsed = mDOMContentFlushed - mNavigationStart;
     nsAutoCString spec;
     if (mLoadedURI) {
       mLoadedURI->GetSpec(spec);
     }
     nsPrintfCString marker(
         "DOMContentFlushed after %dms for URL %s, %s",
         int(elapsed.ToMilliseconds()), spec.get(),
         mDocShellHasBeenActiveSinceNavigationStart
             ? "foreground tab"
             : "this tab was inactive some of the time between navigation start "
               "and DOMContentFlushed");
-    profiler_add_marker(marker.get());
+    DECLARE_DOCSHELL_AND_HISTORY_ID(mDocShell);
+    PAGELOAD_LOG(("%s", marker.get()));
+    profiler_add_marker("DOMContentFlushed",
+                        MakeUnique<TextMarkerPayload>(
+                            marker, mNavigationStart, mDOMContentFlushed,
+                            docShellId, docShellHistoryId));
   }
 #endif
 }
 
 void nsDOMNavigationTiming::NotifyDocShellStateChanged(
     DocShellState aDocShellState) {
   mDocShellHasBeenActiveSinceNavigationStart &=
       (aDocShellState == DocShellState::eActive);
--- a/gfx/layers/wr/StackingContextHelper.h
+++ b/gfx/layers/wr/StackingContextHelper.h
@@ -50,20 +50,16 @@ class MOZ_RAII StackingContextHelper {
   StackingContextHelper();
 
   // Pops the stacking context, if one was pushed during the constructor.
   ~StackingContextHelper();
 
   // Export the inherited scale
   gfx::Size GetInheritedScale() const { return mScale; }
 
-  const gfx::Matrix& GetInheritedTransform() const {
-    return mInheritedTransform;
-  }
-
   const gfx::Matrix& GetSnappingSurfaceTransform() const {
     return mSnappingSurfaceTransform;
   }
 
   const Maybe<nsDisplayTransform*>& GetDeferredTransformItem() const;
   Maybe<gfx::Matrix4x4> GetDeferredTransformMatrix() const;
 
   bool AffectsClipPositioning() const { return mAffectsClipPositioning; }
--- a/image/Image.cpp
+++ b/image/Image.cpp
@@ -257,17 +257,17 @@ ImgDrawResult ImageResource::GetImageCon
   }
 
   SetCurrentImage(container, surface, Nothing());
   entry->mLastDrawResult = drawResult;
   container.forget(aOutContainer);
   return drawResult;
 }
 
-void ImageResource::UpdateImageContainer(const Maybe<IntRect>& aDirtyRect) {
+bool ImageResource::UpdateImageContainer(const Maybe<IntRect>& aDirtyRect) {
   MOZ_ASSERT(NS_IsMainThread());
 
   for (int i = mImageContainers.Length() - 1; i >= 0; --i) {
     ImageContainerEntry& entry = mImageContainers[i];
     RefPtr<ImageContainer> container = entry.mContainer.get();
     if (container) {
       IntSize bestSize;
       RefPtr<SourceSurface> surface;
@@ -284,16 +284,18 @@ void ImageResource::UpdateImageContainer
         IntRect dirtyRect(IntPoint(0, 0), bestSize);
         SetCurrentImage(container, surface, Some(dirtyRect));
       }
     } else {
       // Stop tracking if our weak pointer to the image container was freed.
       mImageContainers.RemoveElementAt(i);
     }
   }
+
+  return !mImageContainers.IsEmpty();
 }
 
 void ImageResource::ReleaseImageContainer() {
   MOZ_ASSERT(NS_IsMainThread());
   mImageContainers.Clear();
 }
 
 // Constructor
--- a/image/Image.h
+++ b/image/Image.h
@@ -363,17 +363,22 @@ class ImageResource : public Image {
   }
 
   ImgDrawResult GetImageContainerImpl(layers::LayerManager* aManager,
                                       const gfx::IntSize& aSize,
                                       const Maybe<SVGImageContext>& aSVGContext,
                                       uint32_t aFlags,
                                       layers::ImageContainer** aContainer);
 
-  void UpdateImageContainer(const Maybe<gfx::IntRect>& aDirtyRect);
+  /**
+   * Re-requests the appropriate frames for each image container using
+   * GetFrameInternal.
+   * @returns True if any image containers were updated, else false.
+   */
+  bool UpdateImageContainer(const Maybe<gfx::IntRect>& aDirtyRect);
 
   void ReleaseImageContainer();
 
  private:
   void SetCurrentImage(layers::ImageContainer* aContainer,
                        gfx::SourceSurface* aSurface,
                        const Maybe<gfx::IntRect>& aDirtyRect);
 
--- a/image/VectorImage.cpp
+++ b/image/VectorImage.cpp
@@ -537,44 +537,52 @@ VectorImage::RequestRefresh(const TimeSt
     tracker->TriggerPendingAnimationsOnNextTick(aTime);
   }
 
   EvaluateAnimation();
 
   mSVGDocumentWrapper->TickRefreshDriver();
 
   if (mHasPendingInvalidation) {
-    mHasPendingInvalidation = false;
     SendInvalidationNotifications();
   }
 }
 
 void VectorImage::SendInvalidationNotifications() {
   // Animated images don't send out invalidation notifications as soon as
   // they're generated. Instead, InvalidateObserversOnNextRefreshDriverTick
   // records that there are pending invalidations and then returns immediately.
   // The notifications are actually sent from RequestRefresh(). We send these
   // notifications there to ensure that there is actually a document observing
   // us. Otherwise, the notifications are just wasted effort.
   //
-  // Non-animated images call this method directly from
+  // Non-animated images post an event to call this method from
   // InvalidateObserversOnNextRefreshDriverTick, because RequestRefresh is never
   // called for them. Ordinarily this isn't needed, since we send out
   // invalidation notifications in OnSVGDocumentLoaded, but in rare cases the
   // SVG document may not be 100% ready to render at that time. In those cases
   // we would miss the subsequent invalidations if we didn't send out the
-  // notifications directly in |InvalidateObservers...|.
+  // notifications indirectly in |InvalidateObservers...|.
+
+  MOZ_ASSERT(mHasPendingInvalidation);
+  mHasPendingInvalidation = false;
+  SurfaceCache::RemoveImage(ImageKey(this));
+
+  if (UpdateImageContainer(Nothing())) {
+    // If we have image containers, that means we probably won't get a Draw call
+    // from the owner since they are using the container. We must assume all
+    // invalidations need to be handled.
+    MOZ_ASSERT(mRenderingObserver, "Should have a rendering observer by now");
+    mRenderingObserver->ResumeHonoringInvalidations();
+  }
 
   if (mProgressTracker) {
-    SurfaceCache::RemoveImage(ImageKey(this));
     mProgressTracker->SyncNotifyProgress(FLAG_FRAME_COMPLETE,
                                          GetMaxSizedIntRect());
   }
-
-  UpdateImageContainer(Nothing());
 }
 
 NS_IMETHODIMP_(IntRect)
 VectorImage::GetImageSpaceInvalidationRect(const IntRect& aRect) {
   return aRect;
 }
 
 //******************************************************************************
@@ -1460,21 +1468,47 @@ VectorImage::OnDataAvailable(nsIRequest*
   return mSVGDocumentWrapper->OnDataAvailable(aRequest, aCtxt, aInStr,
                                               aSourceOffset, aCount);
 }
 
 // --------------------------
 // Invalidation helper method
 
 void VectorImage::InvalidateObserversOnNextRefreshDriverTick() {
+  if (mHasPendingInvalidation) {
+    return;
+  }
+
+  mHasPendingInvalidation = true;
+
+  // Animated images can wait for the refresh tick.
   if (mHaveAnimations) {
-    mHasPendingInvalidation = true;
+    return;
+  }
+
+  // Non-animated images won't get the refresh tick, so we should just send an
+  // invalidation outside the current execution context. We need to defer
+  // because the layout tree is in the middle of invalidation, and the tree
+  // state needs to be consistent. Specifically only some of the frames have
+  // had the NS_FRAME_DESCENDANT_NEEDS_PAINT and/or NS_FRAME_NEEDS_PAINT bits
+  // set by InvalidateFrameInternal in layout/generic/nsFrame.cpp. These bits
+  // get cleared when we repaint the SVG into a surface by
+  // nsIFrame::ClearInvalidationStateBits in nsDisplayList::PaintRoot.
+  nsCOMPtr<nsIEventTarget> eventTarget;
+  if (mProgressTracker) {
+    eventTarget = mProgressTracker->GetEventTarget();
   } else {
-    SendInvalidationNotifications();
+    eventTarget = do_GetMainThread();
   }
+
+  RefPtr<VectorImage> self(this);
+  nsCOMPtr<nsIRunnable> ev(NS_NewRunnableFunction(
+      "VectorImage::SendInvalidationNotifications",
+      [=]() -> void { self->SendInvalidationNotifications(); }));
+  eventTarget->Dispatch(ev.forget(), NS_DISPATCH_NORMAL);
 }
 
 void VectorImage::PropagateUseCounters(Document* aParentDocument) {
   Document* doc = mSVGDocumentWrapper->GetDocument();
   if (doc) {
     doc->PropagateUseCounters(aParentDocument);
   }
 }
--- a/js/src/jit/IonBuilder.cpp
+++ b/js/src/jit/IonBuilder.cpp
@@ -4791,16 +4791,20 @@ AbortReasonOr<Ok> IonBuilder::inlineCall
     CallInfo inlineInfo(alloc(), pc, callInfo.constructing(),
                         callInfo.ignoresReturnValue());
     if (!inlineInfo.init(callInfo)) {
       return abort(AbortReason::Alloc);
     }
     inlineInfo.popCallStack(inlineBlock);
     inlineInfo.setFun(funcDef);
 
+    if (callInfo.constructing() && callInfo.getNewTarget() == callInfo.fun()) {
+      inlineInfo.setNewTarget(funcDef);
+    }
+
     if (maybeCache) {
       // Assign the 'this' value a TypeSet specialized to the groups that
       // can generate this inlining target.
       MOZ_ASSERT(callInfo.thisArg() == maybeCache->value());
       TemporaryTypeSet* thisTypes =
           maybeCache->propTable()->buildTypeSetForFunction(alloc(), target);
       if (!thisTypes) {
         return abort(AbortReason::Alloc);
--- a/js/src/jit/arm64/CodeGenerator-arm64.cpp
+++ b/js/src/jit/arm64/CodeGenerator-arm64.cpp
@@ -1451,17 +1451,17 @@ void CodeGenerator::visitCompareBitwiseA
 }
 
 void CodeGenerator::visitBitAndAndBranch(LBitAndAndBranch* baab) {
   if (baab->right()->isConstant()) {
     masm.Tst(toWRegister(baab->left()), Operand(ToInt32(baab->right())));
   } else {
     masm.Tst(toWRegister(baab->left()), toWRegister(baab->right()));
   }
-  emitBranch(Assembler::NonZero, baab->ifTrue(), baab->ifFalse());
+  emitBranch(baab->cond(), baab->ifTrue(), baab->ifFalse());
 }
 
 void CodeGenerator::visitWasmUint32ToDouble(LWasmUint32ToDouble* lir) {
   masm.convertUInt32ToDouble(ToRegister(lir->input()),
                              ToFloatRegister(lir->output()));
 }
 
 void CodeGenerator::visitWasmUint32ToFloat32(LWasmUint32ToFloat32* lir) {
--- a/js/src/tests/lib/wptreport.py
+++ b/js/src/tests/lib/wptreport.py
@@ -61,17 +61,17 @@ class WptreportHandler(object):
         end_time = time()
         start_time = end_time - duration
 
         self.formatter.test_start({
             "test": testname,
             "time": start_time,
         })
 
-        for result in result["subtests"]:
-            self.formatter.test_status(result)
+        for subtest in result["subtests"]:
+            self.formatter.test_status(subtest)
 
         self.formatter.test_end({
             "test": testname,
             "time": end_time,
             "status": result["status"],
             "expected": result["expected"],
         })
--- a/js/src/wasm/WasmCompile.cpp
+++ b/js/src/wasm/WasmCompile.cpp
@@ -408,41 +408,38 @@ void CompilerEnvironment::computeParamet
     computeParameters(gcFeatureOptIn);
     return;
   }
 
   bool gcEnabled = args_->gcTypesConfigured == HasGcTypes::True &&
                    gcFeatureOptIn == HasGcTypes::True;
   bool argBaselineEnabled = args_->baselineEnabled || gcEnabled;
   bool argIonEnabled = args_->ionEnabled && !gcEnabled;
-  bool argTestTiering = args_->testTiering && !gcEnabled;
   bool argDebugEnabled = args_->debugEnabled;
 
   uint32_t codeSectionSize = 0;
 
   SectionRange range;
   if (StartsCodeSection(d.begin(), d.end(), &range)) {
     codeSectionSize = range.size;
   }
 
   // Attempt to default to ion if baseline is disabled.
-  bool baselineEnabled =
-      BaselineCanCompile() && (argBaselineEnabled || argTestTiering);
+  bool baselineEnabled = BaselineCanCompile() && argBaselineEnabled;
   bool debugEnabled = BaselineCanCompile() && argDebugEnabled;
-  bool ionEnabled =
-      IonCanCompile() && (argIonEnabled || !baselineEnabled || argTestTiering);
+  bool ionEnabled = IonCanCompile() && (argIonEnabled || !baselineEnabled);
 #ifdef ENABLE_WASM_CRANELIFT
   bool forceCranelift = args_->forceCranelift;
 #endif
 
   // HasCompilerSupport() should prevent failure here
   MOZ_RELEASE_ASSERT(baselineEnabled || ionEnabled);
 
   if (baselineEnabled && ionEnabled && !debugEnabled && CanUseExtraThreads() &&
-      (TieringBeneficial(codeSectionSize) || argTestTiering)) {
+      (TieringBeneficial(codeSectionSize) || args_->testTiering)) {
     mode_ = CompileMode::Tier1;
     tier_ = Tier::Baseline;
   } else {
     mode_ = CompileMode::Once;
     tier_ = debugEnabled || !ionEnabled ? Tier::Baseline : Tier::Optimized;
   }
 
   optimizedBackend_ = OptimizedBackend::Ion;
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -6356,18 +6356,16 @@ static SnappedImageDrawingParameters Com
 
   gfxMatrix currentMatrix = aCtx->CurrentMatrixDouble();
   gfxRect fill = devPixelFill;
   gfxRect dest = devPixelDest;
   bool didSnap;
   // Snap even if we have a scale in the context. But don't snap if
   // we have something that's not translation+scale, or if the scale flips in
   // the X or Y direction, because snapped image drawing can't handle that yet.
-  // Any changes to this algorithm will need to be reflected in
-  // ComputeImageContainerDrawingParameters.
   if (!currentMatrix.HasNonAxisAlignedTransform() && currentMatrix._11 > 0.0 &&
       currentMatrix._22 > 0.0 && aCtx->UserToDevicePixelSnapped(fill, true) &&
       aCtx->UserToDevicePixelSnapped(dest, true)) {
     // We snapped. On this code path, |fill| and |dest| take into account
     // currentMatrix's transform.
     didSnap = true;
   } else {
     // We didn't snap. On this code path, |fill| and |dest| do not take into
@@ -6747,56 +6745,27 @@ static ImgDrawResult DrawImageInternal(
     CSSIntSize cssViewportSize(viewportSize.width, viewportSize.height);
     if (!aSVGContext) {
       aSVGContext.emplace(Some(cssViewportSize));
     } else {
       aSVGContext->SetViewportSize(Some(cssViewportSize));
     }
   }
 
-  // Attempt to snap pixels, the same as ComputeSnappedImageDrawingParameters.
-  // Any changes to the algorithm here will need to be reflected there.
-  bool snapped = false;
-  gfxSize gfxLayerSize;
-  const gfx::Matrix& itm = aSc.GetInheritedTransform();
-  if (!itm.HasNonAxisAlignedTransform() && itm._11 > 0.0 && itm._22 > 0.0) {
-    gfxRect rect(gfxPoint(aDestRect.X(), aDestRect.Y()),
-                 gfxSize(aDestRect.Width(), aDestRect.Height()));
-
-    gfxPoint p1 = ThebesPoint(itm.TransformPoint(ToPoint(rect.TopLeft())));
-    gfxPoint p2 = ThebesPoint(itm.TransformPoint(ToPoint(rect.TopRight())));
-    gfxPoint p3 = ThebesPoint(itm.TransformPoint(ToPoint(rect.BottomRight())));
-
-    if (p2 == gfxPoint(p1.x, p3.y) || p2 == gfxPoint(p3.x, p1.y)) {
-      p1.Round();
-      p3.Round();
-
-      rect.MoveTo(gfxPoint(std::min(p1.x, p3.x), std::min(p1.y, p3.y)));
-      rect.SizeTo(gfxSize(std::max(p1.x, p3.x) - rect.X(),
-                          std::max(p1.y, p3.y) - rect.Y()));
-
-      // An empty size is unacceptable so we ensure our suggested size is at
-      // least 1 pixel wide/tall.
-      gfxLayerSize =
-          gfxSize(std::max(rect.Width(), 1.0), std::max(rect.Height(), 1.0));
-      snapped = true;
-    }
-  }
-
-  if (!snapped) {
-    // Compute our size in layer pixels.
-    const LayerIntSize layerSize =
-        RoundedToInt(LayerSize(aDestRect.Width() * scaleFactors.width,
-                               aDestRect.Height() * scaleFactors.height));
-
-    // An empty size is unacceptable so we ensure our suggested size is at least
-    // 1 pixel wide/tall.
-    gfxLayerSize =
-        gfxSize(std::max(layerSize.width, 1), std::max(layerSize.height, 1));
-  }
+  // Compute our size in layer pixels. We may need to revisit this for Android
+  // because mobile websites are rarely displayed at a 1:1
+  // LayoutPixel:ScreenPixel ratio and the snapping here may be insufficient.
+  const LayerIntSize layerSize =
+      RoundedToInt(LayerSize(aDestRect.Width() * scaleFactors.width,
+                             aDestRect.Height() * scaleFactors.height));
+
+  // An empty size is unacceptable so we ensure our suggested size is at least
+  // 1 pixel wide/tall.
+  gfxSize gfxLayerSize =
+      gfxSize(std::max(layerSize.width, 1), std::max(layerSize.height, 1));
 
   return aImage->OptimalImageSizeForDest(
       gfxLayerSize, imgIContainer::FRAME_CURRENT, samplingFilter, aFlags);
 }
 
 /* static */ nsPoint nsLayoutUtils::GetBackgroundFirstTilePos(
     const nsPoint& aDest, const nsPoint& aFill, const nsSize& aRepeatSize) {
   return nsPoint(NSToIntFloor(float(aFill.x - aDest.x) / aRepeatSize.width) *
--- a/layout/generic/ScrollAnchorContainer.cpp
+++ b/layout/generic/ScrollAnchorContainer.cpp
@@ -97,16 +97,24 @@ static nsRect FindScrollAnchoringBoundin
       nsRect transformed = nsLayoutUtils::TransformFrameRectToAncestor(
           continuation, localRect, aScrollFrame);
       bounding = bounding.Union(transformed);
     }
     return bounding;
   }
 
   nsRect localRect = aCandidate->GetScrollableOverflowRectRelativeToSelf();
+
+  // XXX this isn't correct with non-vertical-tb writing-mode, see bug 1520344
+  if (localRect.X() < 0) {
+    localRect.SetBoxX(0, localRect.XMost());
+  }
+  if (localRect.Y() < 0) {
+    localRect.SetBoxY(0, localRect.YMost());
+  }
   nsRect transformed = nsLayoutUtils::TransformFrameRectToAncestor(
       aCandidate, localRect, aScrollFrame);
   return transformed;
 }
 
 void ScrollAnchorContainer::SelectAnchor() {
   MOZ_ASSERT(mScrollFrame->mScrolledFrame);
   MOZ_ASSERT(mAnchorNodeIsDirty);
--- a/layout/generic/nsBulletFrame.cpp
+++ b/layout/generic/nsBulletFrame.cpp
@@ -427,16 +427,18 @@ ImgDrawResult BulletRenderer::CreateWebR
   if (aDisplayListBuilder->ShouldSyncDecodeImages()) {
     flags |= imgIContainer::FLAG_SYNC_DECODE;
   }
 
   const int32_t appUnitsPerDevPixel =
       aItem->Frame()->PresContext()->AppUnitsPerDevPixel();
   LayoutDeviceRect destRect =
       LayoutDeviceRect::FromAppUnits(mDest, appUnitsPerDevPixel);
+  destRect.Round();
+
   Maybe<SVGImageContext> svgContext;
   gfx::IntSize decodeSize =
       nsLayoutUtils::ComputeImageContainerDrawingParameters(
           mImage, aItem->Frame(), destRect, aSc, flags, svgContext);
 
   RefPtr<layers::ImageContainer> container;
   ImgDrawResult drawResult = mImage->GetImageContainerAtSize(
       aManager->LayerManager(), decodeSize, svgContext, flags,
@@ -449,17 +451,17 @@ ImgDrawResult BulletRenderer::CreateWebR
       nsLayoutUtils::GetSamplingFilterForFrame(aItem->Frame()));
   gfx::IntSize size;
   Maybe<wr::ImageKey> key = aManager->CommandBuilder().CreateImageKey(
       aItem, container, aBuilder, aResources, rendering, aSc, size, Nothing());
   if (key.isNothing()) {
     return drawResult;
   }
 
-  wr::LayoutRect dest = wr::ToRoundedLayoutRect(destRect);
+  wr::LayoutRect dest = wr::ToLayoutRect(destRect);
 
   aBuilder.PushImage(dest, dest, !aItem->BackfaceIsHidden(), rendering,
                      key.value());
 
   return drawResult;
 }
 
 bool BulletRenderer::CreateWebRenderCommandsForPath(
--- a/layout/generic/nsImageFrame.cpp
+++ b/layout/generic/nsImageFrame.cpp
@@ -1613,18 +1613,20 @@ ImgDrawResult nsImageFrame::DisplayAltFe
       nsCOMPtr<imgIContainer> imgCon;
       request->GetImage(getter_AddRefs(imgCon));
       MOZ_ASSERT(imgCon, "Load complete, but no image container?");
 
       nsRect dest(flushRight ? inner.XMost() - size : inner.x, inner.y, size,
                   size);
 
       const int32_t factor = PresContext()->AppUnitsPerDevPixel();
-      const LayoutDeviceRect destRect(
+      LayoutDeviceRect destRect(
           LayoutDeviceRect::FromAppUnits(dest, factor));
+      destRect.Round();
+
       Maybe<SVGImageContext> svgContext;
       IntSize decodeSize =
           nsLayoutUtils::ComputeImageContainerDrawingParameters(
               imgCon, this, destRect, aSc, aFlags, svgContext);
       RefPtr<ImageContainer> container;
       result = imgCon->GetImageContainerAtSize(
           aManager->LayerManager(), decodeSize, svgContext, aFlags, getter_AddRefs(container));
       if (container) {
@@ -1891,18 +1893,20 @@ bool nsDisplayImage::CreateWebRenderComm
   if (aDisplayListBuilder->IsPaintingToWindow()) {
     flags |= imgIContainer::FLAG_HIGH_QUALITY_SCALING;
   }
   if (aDisplayListBuilder->ShouldSyncDecodeImages()) {
     flags |= imgIContainer::FLAG_SYNC_DECODE;
   }
 
   const int32_t factor = mFrame->PresContext()->AppUnitsPerDevPixel();
-  const LayoutDeviceRect destRect(
+  LayoutDeviceRect destRect(
       LayoutDeviceRect::FromAppUnits(GetDestRect(), factor));
+  destRect.Round();
+
   Maybe<SVGImageContext> svgContext;
   IntSize decodeSize = nsLayoutUtils::ComputeImageContainerDrawingParameters(
       mImage, mFrame, destRect, aSc, flags, svgContext);
 
   RefPtr<layers::ImageContainer> container;
   ImgDrawResult drawResult = mImage->GetImageContainerAtSize(
       aManager->LayerManager(), decodeSize, svgContext, flags,
       getter_AddRefs(container));
--- a/layout/painting/nsCSSRenderingBorders.cpp
+++ b/layout/painting/nsCSSRenderingBorders.cpp
@@ -3548,17 +3548,18 @@ ImgDrawResult nsCSSBorderImageRenderer::
   NS_FOR_CSS_SIDES(i) {
     slice[i] = (float)(mSlice.Side(i)) / appUnitsPerDevPixel;
     widths[i] = (float)(mWidths.Side(i)) / appUnitsPerDevPixel;
     outset[i] = (float)(mImageOutset.Side(i)) / appUnitsPerDevPixel;
   }
 
   LayoutDeviceRect destRect =
       LayoutDeviceRect::FromAppUnits(mArea, appUnitsPerDevPixel);
-  wr::LayoutRect dest = wr::ToRoundedLayoutRect(destRect);
+  destRect.Round();
+  wr::LayoutRect dest = wr::ToLayoutRect(destRect);
 
   wr::LayoutRect clip = dest;
   if (!mClip.IsEmpty()) {
     LayoutDeviceRect clipRect =
         LayoutDeviceRect::FromAppUnits(mClip, appUnitsPerDevPixel);
     clip = wr::ToRoundedLayoutRect(clipRect);
   }
 
--- a/layout/painting/nsImageRenderer.cpp
+++ b/layout/painting/nsImageRenderer.cpp
@@ -562,16 +562,19 @@ ImgDrawResult nsImageRenderer::BuildWebR
       CSSIntSize imageSize(nsPresContext::AppUnitsToIntCSSPixels(mSize.width),
                            nsPresContext::AppUnitsToIntCSSPixels(mSize.height));
       Maybe<SVGImageContext> svgContext(Some(SVGImageContext(Some(imageSize))));
 
       const int32_t appUnitsPerDevPixel =
           mForFrame->PresContext()->AppUnitsPerDevPixel();
       LayoutDeviceRect destRect =
           LayoutDeviceRect::FromAppUnits(aDest, appUnitsPerDevPixel);
+      auto stretchSize = wr::ToLayoutSize(destRect.Size());
+      destRect.Round();
+
       gfx::IntSize decodeSize =
           nsLayoutUtils::ComputeImageContainerDrawingParameters(
               mImageContainer, mForFrame, destRect, aSc, containerFlags,
               svgContext);
 
       RefPtr<layers::ImageContainer> container;
       drawResult = mImageContainer->GetImageContainerAtSize(
           aManager->LayerManager(), decodeSize, svgContext, containerFlags,
@@ -595,18 +598,17 @@ ImgDrawResult nsImageRenderer::BuildWebR
       nsPoint firstTilePos = nsLayoutUtils::GetBackgroundFirstTilePos(
           aDest.TopLeft(), aFill.TopLeft(), aRepeatSize);
       LayoutDeviceRect fillRect = LayoutDeviceRect::FromAppUnits(
           nsRect(firstTilePos.x, firstTilePos.y, aFill.XMost() - firstTilePos.x,
                  aFill.YMost() - firstTilePos.y),
           appUnitsPerDevPixel);
       wr::LayoutRect fill = wr::ToRoundedLayoutRect(fillRect);
 
-      wr::LayoutRect roundedDest = wr::ToRoundedLayoutRect(destRect);
-      auto stretchSize = wr::ToLayoutSize(destRect.Size());
+      wr::LayoutRect roundedDest = wr::ToLayoutRect(destRect);
 
       // WebRender special cases situations where stretchSize == fillSize to
       // infer that it shouldn't use repeat sampling. This makes sure
       // we hit those special cases when not repeating.
 
       switch (mExtendMode) {
         case ExtendMode::CLAMP:
           fill = roundedDest;
--- a/layout/xul/nsImageBoxFrame.cpp
+++ b/layout/xul/nsImageBoxFrame.cpp
@@ -393,16 +393,18 @@ ImgDrawResult nsImageBoxFrame::CreateWeb
   }
   if (aFlags & nsImageRenderer::FLAG_SYNC_DECODE_IMAGES) {
     containerFlags |= imgIContainer::FLAG_SYNC_DECODE;
   }
 
   const int32_t appUnitsPerDevPixel = PresContext()->AppUnitsPerDevPixel();
   LayoutDeviceRect fillRect =
       LayoutDeviceRect::FromAppUnits(dest, appUnitsPerDevPixel);
+  fillRect.Round();
+
   Maybe<SVGImageContext> svgContext;
   gfx::IntSize decodeSize =
       nsLayoutUtils::ComputeImageContainerDrawingParameters(
           imgCon, aItem->Frame(), fillRect, aSc, containerFlags, svgContext);
 
   RefPtr<layers::ImageContainer> container;
   result = imgCon->GetImageContainerAtSize(aManager->LayerManager(), decodeSize,
                                            svgContext, containerFlags,
@@ -415,17 +417,17 @@ ImgDrawResult nsImageBoxFrame::CreateWeb
   mozilla::wr::ImageRendering rendering = wr::ToImageRendering(
       nsLayoutUtils::GetSamplingFilterForFrame(aItem->Frame()));
   gfx::IntSize size;
   Maybe<wr::ImageKey> key = aManager->CommandBuilder().CreateImageKey(
       aItem, container, aBuilder, aResources, rendering, aSc, size, Nothing());
   if (key.isNothing()) {
     return result;
   }
-  wr::LayoutRect fill = wr::ToRoundedLayoutRect(fillRect);
+  wr::LayoutRect fill = wr::ToLayoutRect(fillRect);
 
   LayoutDeviceSize gapSize(0, 0);
   aBuilder.PushImage(fill, fill, !BackfaceIsHidden(),
                      wr::ToLayoutSize(fillRect.Size()),
                      wr::ToLayoutSize(gapSize), rendering, key.value());
 
   return result;
 }
--- a/layout/xul/reftest/reftest.list
+++ b/layout/xul/reftest/reftest.list
@@ -1,9 +1,9 @@
 fails-if(Android) == textbox-multiline-noresize.xul textbox-multiline-ref.xul # reference is blank on Android (due to no native theme support?)
 != textbox-multiline-resize.xul textbox-multiline-ref.xul
 == popup-explicit-size.xul popup-explicit-size-ref.xul
-random-if(Android) fuzzy-if(webrender,128-128,168-168) == image-size.xul image-size-ref.xul
+random-if(Android) == image-size.xul image-size-ref.xul
 == image-scaling-min-height-1.xul image-scaling-min-height-1-ref.xul
 == textbox-text-transform.xul textbox-text-transform-ref.xul
 
 == checkbox-dynamic-change.xul checkbox-dynamic-change-ref.xul
 == radio-dynamic-change.xul radio-dynamic-change-ref.xul
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -3333,17 +3333,17 @@ pref("dom.ipc.plugins.reportCrashURL", t
 pref("dom.ipc.plugins.unloadTimeoutSecs", 30);
 
 // Allow Flash async drawing mode in 64-bit release builds
 pref("dom.ipc.plugins.asyncdrawing.enabled", true);
 // Force the accelerated direct path for a subset of Flash wmode values
 pref("dom.ipc.plugins.forcedirect.enabled", true);
 
 // Enable multi by default.
-#if defined(NIGHTLY_BUILD) && !defined(MOZ_ASAN)
+#if !defined(MOZ_ASAN)
 pref("dom.ipc.processCount", 8);
 #else
 pref("dom.ipc.processCount", 4);
 #endif
 
 // Default to allow only one file:// URL content process.
 pref("dom.ipc.processCount.file", 1);
 
--- a/python/mozboot/mozboot/osx.py
+++ b/python/mozboot/mozboot/osx.py
@@ -19,24 +19,25 @@ from distutils.version import StrictVers
 from mozboot.base import BaseBootstrapper
 
 HOMEBREW_BOOTSTRAP = 'https://raw.githubusercontent.com/Homebrew/install/master/install'
 XCODE_APP_STORE = 'macappstore://itunes.apple.com/app/id497799835?mt=12'
 XCODE_LEGACY = ('https://developer.apple.com/downloads/download.action?path=Developer_Tools/'
                 'xcode_3.2.6_and_ios_sdk_4.3__final/xcode_3.2.6_and_ios_sdk_4.3.dmg')
 
 MACPORTS_URL = {
-    '13': 'https://distfiles.macports.org/MacPorts/MacPorts-2.5.3-10.13-HighSierra.pkg',
-    '12': 'https://distfiles.macports.org/MacPorts/MacPorts-2.5.3-10.12-Sierra.pkg',
-    '11': 'https://distfiles.macports.org/MacPorts/MacPorts-2.5.3-10.11-ElCapitan.pkg',
-    '10': 'https://distfiles.macports.org/MacPorts/MacPorts-2.5.3-10.10-Yosemite.pkg',
-    '9': 'https://distfiles.macports.org/MacPorts/MacPorts-2.5.3-10.9-Mavericks.pkg',
-    '8': 'https://distfiles.macports.org/MacPorts/MacPorts-2.5.3-10.8-MountainLion.pkg',
-    '7': 'https://distfiles.macports.org/MacPorts/MacPorts-2.5.3-10.7-Lion.pkg',
-    '6': 'https://distfiles.macports.org/MacPorts/MacPorts-2.5.3-10.6-SnowLeopard.pkg', }
+    '14': 'https://distfiles.macports.org/MacPorts/MacPorts-2.5.4-10.14-Mojave.pkg',
+    '13': 'https://distfiles.macports.org/MacPorts/MacPorts-2.5.4-10.13-HighSierra.pkg',
+    '12': 'https://distfiles.macports.org/MacPorts/MacPorts-2.5.4-10.12-Sierra.pkg',
+    '11': 'https://distfiles.macports.org/MacPorts/MacPorts-2.5.4-10.11-ElCapitan.pkg',
+    '10': 'https://distfiles.macports.org/MacPorts/MacPorts-2.5.4-10.10-Yosemite.pkg',
+    '9': 'https://distfiles.macports.org/MacPorts/MacPorts-2.5.4-10.9-Mavericks.pkg',
+    '8': 'https://distfiles.macports.org/MacPorts/MacPorts-2.5.4-10.8-MountainLion.pkg',
+    '7': 'https://distfiles.macports.org/MacPorts/MacPorts-2.5.4-10.7-Lion.pkg',
+    '6': 'https://distfiles.macports.org/MacPorts/MacPorts-2.5.4-10.6-SnowLeopard.pkg', }
 
 RE_CLANG_VERSION = re.compile('Apple (?:clang|LLVM) version (\d+\.\d+)')
 
 APPLE_CLANG_MINIMUM_VERSION = StrictVersion('4.2')
 
 XCODE_REQUIRED = '''
 Xcode is required to build Firefox. Please complete the install of Xcode
 through the App Store.
--- a/taskcluster/taskgraph/actions/cancel_all.py
+++ b/taskcluster/taskgraph/actions/cancel_all.py
@@ -5,22 +5,23 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import, print_function, unicode_literals
 
 import concurrent.futures as futures
 import logging
 import os
 
-from taskgraph.util.taskcluster import list_task_group, cancel_task
+from taskgraph.util.taskcluster import (
+    list_task_group_incomplete_tasks,
+    cancel_task,
+    CONCURRENCY,
+)
 from .registry import register_callback_action
 
-# the maximum number of parallel cancelTask calls to make
-CONCURRENCY = 50
-
 logger = logging.getLogger(__name__)
 
 
 @register_callback_action(
     title='Cancel All',
     name='cancel-all',
     kind='hook',
     generic=True,
@@ -28,16 +29,19 @@ logger = logging.getLogger(__name__)
     description=(
         'Cancel all running and pending tasks created by the decision task '
         'this action task is associated with.'
     ),
     order=400,
     context=[]
 )
 def cancel_all_action(parameters, graph_config, input, task_group_id, task_id, task):
+    def do_cancel_task(task_id):
+        logger.info('Cancelling task {}'.format(task_id))
+        cancel_task(task_id, use_proxy=True)
+
     own_task_id = os.environ.get('TASK_ID', '')
+    to_cancel = [t for t in list_task_group_incomplete_tasks(task_group_id) if t != own_task_id]
+    logger.info("Cancelling {} tasks".format(len(to_cancel)))
     with futures.ThreadPoolExecutor(CONCURRENCY) as e:
-        cancels_jobs = [
-            e.submit(cancel_task, t, use_proxy=True)
-            for t in list_task_group(task_group_id) if t != own_task_id
-        ]
-        for job in cancels_jobs:
-            job.result()
+        cancel_futs = [e.submit(do_cancel_task, t) for t in to_cancel]
+        for f in futures.as_completed(cancel_futs):
+            f.result()
--- a/taskcluster/taskgraph/create.py
+++ b/taskcluster/taskgraph/create.py
@@ -1,48 +1,34 @@
 # 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
 
 import concurrent.futures as futures
-import requests
-import requests.adapters
 import json
 import os
 import sys
 import logging
 
 from slugid import nice as slugid
 from taskgraph.util.parameterization import resolve_timestamps
 from taskgraph.util.time import current_json_time
+from taskgraph.util.taskcluster import get_session, CONCURRENCY
 
 logger = logging.getLogger(__name__)
 
-# the maximum number of parallel createTask calls to make
-CONCURRENCY = 50
-
 # this is set to true for `mach taskgraph action-callback --test`
 testing = False
 
 
 def create_tasks(taskgraph, label_to_taskid, params, decision_task_id=None):
     taskid_to_label = {t: l for l, t in label_to_taskid.iteritems()}
 
-    session = requests.Session()
-
-    # Default HTTPAdapter uses 10 connections. Mount custom adapter to increase
-    # that limit. Connections are established as needed, so using a large value
-    # should not negatively impact performance.
-    http_adapter = requests.adapters.HTTPAdapter(pool_connections=CONCURRENCY,
-                                                 pool_maxsize=CONCURRENCY)
-    session.mount('https://', http_adapter)
-    session.mount('http://', http_adapter)
-
     decision_task_id = decision_task_id or os.environ.get('TASK_ID')
 
     # when running as an actual decision task, we use the decision task's
     # taskId as the taskGroupId.  The process that created the decision task
     # helpfully placed it in this same taskGroup.  If there is no $TASK_ID,
     # fall back to a slugid
     task_group_id = decision_task_id or slugid()
     scheduler_id = 'gecko-level-{}'.format(params['level'])
@@ -61,16 +47,17 @@ def create_tasks(taskgraph, label_to_tas
             if not any(t in taskgraph.tasks for t in task_def.get('dependencies', [])):
                 task_def.setdefault('dependencies', []).append(decision_task_id)
 
         task_def['taskGroupId'] = task_group_id
         task_def['schedulerId'] = scheduler_id
 
     # If `testing` is True, then run without parallelization
     concurrency = CONCURRENCY if not testing else 1
+    session = get_session()
     with futures.ThreadPoolExecutor(concurrency) as e:
         fs = {}
 
         # We can't submit a task until its dependencies have been submitted.
         # So our strategy is to walk the graph and submit tasks once all
         # their dependencies have been submitted.
         tasklist = set(taskgraph.graph.visit_postorder())
         alltasks = tasklist.copy()
--- a/taskcluster/taskgraph/util/taskcluster.py
+++ b/taskcluster/taskgraph/util/taskcluster.py
@@ -10,28 +10,30 @@ import os
 import datetime
 import functools
 import yaml
 import requests
 import logging
 import taskcluster_urls as liburls
 from mozbuild.util import memoize
 from requests.packages.urllib3.util.retry import Retry
-from requests.adapters import HTTPAdapter
 from taskgraph.task import Task
 
 logger = logging.getLogger(__name__)
 
 # this is set to true for `mach taskgraph action-callback --test`
 testing = False
 
 # Default rootUrl to use if none is given in the environment; this should point
 # to the production Taskcluster deployment used for CI.
 PRODUCTION_TASKCLUSTER_ROOT_URL = 'https://taskcluster.net'
 
+# the maximum number of parallel Taskcluster API calls to make
+CONCURRENCY = 50
+
 
 @memoize
 def get_root_url():
     """Get the current TASKCLUSTER_ROOT_URL.  When running in a task, this must
     come from $TASKCLUSTER_ROOT_URL; when run on the command line, we apply a
     defualt that points to the production deployment of Taskcluster."""
     if 'TASKCLUSTER_ROOT_URL' not in os.environ:
         if 'TASK_ID' in os.environ:
@@ -43,29 +45,39 @@ def get_root_url():
         os.environ['TASKCLUSTER_ROOT_URL'],
         ' with taskcluster-proxy' if 'TASKCLUSTER_PROXY_URL' in os.environ else ''))
     return os.environ['TASKCLUSTER_ROOT_URL']
 
 
 @memoize
 def get_session():
     session = requests.Session()
+
     retry = Retry(total=5, backoff_factor=0.1,
                   status_forcelist=[500, 502, 503, 504])
-    session.mount('http://', HTTPAdapter(max_retries=retry))
-    session.mount('https://', HTTPAdapter(max_retries=retry))
+
+    # Default HTTPAdapter uses 10 connections. Mount custom adapter to increase
+    # that limit. Connections are established as needed, so using a large value
+    # should not negatively impact performance.
+    http_adapter = requests.adapters.HTTPAdapter(
+        pool_connections=CONCURRENCY,
+        pool_maxsize=CONCURRENCY,
+        max_retries=retry)
+    session.mount('https://', http_adapter)
+    session.mount('http://', http_adapter)
+
     return session
 
 
 def _do_request(url, force_get=False, **kwargs):
     session = get_session()
     if kwargs and not force_get:
         response = session.post(url, **kwargs)
     else:
-        response = session.get(url, stream=True)
+        response = session.get(url, stream=True, **kwargs)
     if response.status_code >= 400:
         # Consume content before raise_for_status, so that the connection can be
         # reused.
         response.content
     response.raise_for_status()
     return response
 
 
@@ -263,18 +275,18 @@ def send_email(address, subject, content
     _do_request(url, json={
         'address': address,
         'subject': subject,
         'content': content,
         'link': link,
     })
 
 
-def list_task_group(task_group_id):
-    """Generate the tasks in a task group"""
+def list_task_group_incomplete_tasks(task_group_id):
+    """Generate the incomplete tasks in a task group"""
     params = {}
     while True:
         url = liburls.api(get_root_url(), 'queue', 'v1',
                           'task-group/{}/list'.format(task_group_id))
         resp = _do_request(url, force_get=True, params=params).json()
         for task in [t['status'] for t in resp['tasks']]:
             if task['state'] in ['running', 'pending', 'unscheduled']:
                 yield task['taskId']
--- a/testing/mochitest/tests/SimpleTest/SimpleTest.js
+++ b/testing/mochitest/tests/SimpleTest/SimpleTest.js
@@ -985,17 +985,17 @@ SimpleTest.promiseClipboardChange = asyn
         inputValidatorFn = typeof(aExpectedStringOrValidatorFn) == "string"
             ? function(aData) { return aData == aExpectedStringOrValidatorFn; }
             : aExpectedStringOrValidatorFn;
     }
 
     let maxPolls = aTimeout ? aTimeout / 100 : 50;
 
     async function putAndVerify(operationFn, validatorFn, flavor) {
-        operationFn();
+        await operationFn();
 
         let data;
         for (let i = 0; i < maxPolls; i++) {
             data = SpecialPowers.getClipboardData(flavor);
             if (validatorFn(data)) {
                 // Don't show the success message when waiting for preExpectedVal
                 if (preExpectedVal) {
                     preExpectedVal = null;
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/meta/webdriver/tests/new_session/timeouts.py.ini
@@ -0,0 +1,2 @@
+[timeouts.py]
+    disabled: if ccov and (os == "win") and (bits == 64) and (version == "10.0.15063"): https://bugzilla.mozilla.org/show_bug.cgi?id=1495002
--- a/toolkit/content/widgets/richlistbox.js
+++ b/toolkit/content/widgets/richlistbox.js
@@ -5,16 +5,26 @@
 
 // This is loaded into chrome windows with the subscript loader. If you need to
 // define globals, wrap in a block to prevent leaking onto `window`.
 
 MozElements.RichListBox = class RichListBox extends MozElements.BaseControl {
   constructor() {
     super();
 
+    this.selectedItems = new ChromeNodeList();
+    this._currentIndex = null;
+    this._lastKeyTime = 0;
+    this._incrementalString = "";
+    this._suppressOnSelect = false;
+    this._userSelecting = false;
+    this._selectTimeout = null;
+    this._currentItem = null;
+    this._selectionStart = null;
+
     this.addEventListener("keypress", event => {
       if (event.altKey || event.metaKey) {
         return;
       }
 
       switch (event.keyCode) {
         case KeyEvent.DOM_VK_UP:
           this._moveByOffsetFromUserEvent(-1, event);
@@ -130,27 +140,16 @@ MozElements.RichListBox = class RichList
   }
 
   connectedCallback() {
     if (this.delayConnectedCallback()) {
       return;
     }
 
     this.setAttribute("allowevents", "true");
-
-    this.selectedItems = new ChromeNodeList();
-    this._currentIndex = null;
-    this._lastKeyTime = 0;
-    this._incrementalString = "";
-    this._suppressOnSelect = false;
-    this._userSelecting = false;
-    this._selectTimeout = null;
-    this._currentItem = null;
-    this._selectionStart = null;
-
     this._refreshSelection();
   }
 
   // nsIDOMXULSelectControlElement
   set selectedItem(val) {
     this.selectItem(val);
   }
   get selectedItem() {
--- a/toolkit/locales/en-US/toolkit/about/aboutAddons.ftl
+++ b/toolkit/locales/en-US/toolkit/about/aboutAddons.ftl
@@ -276,18 +276,17 @@ extensions-updates-update-selected =
     .label = Install Updates
     .tooltiptext = Install available updates in this list
 
 ## Extension shortcut management
 
 shortcuts-manage =
   .label = Keyboard Shortcuts
 shortcuts-empty-message = There are no shortcuts for this extension.
-# TODO: Confirm this copy.
-shortcuts-no-addons = You don't have any active add-ons.
+shortcuts-no-addons = You don’t have any extensions enabled.
 shortcuts-input =
   .placeholder = Type a shortcut
 
 shortcuts-browserAction = Activate extension
 shortcuts-pageAction = Activate page action
 shortcuts-sidebarAction = Toggle the sidebar
 
 shortcuts-modifier-mac = Include Ctrl, Alt, or ⌘
--- a/tools/profiler/core/ProfilerMarkerPayload.cpp
+++ b/tools/profiler/core/ProfilerMarkerPayload.cpp
@@ -92,16 +92,31 @@ void UserTimingMarkerPayload::StreamPayl
   if (mEndMark.isSome()) {
     aWriter.StringProperty("endMark",
                            NS_ConvertUTF16toUTF8(mEndMark.value()).get());
   } else {
     aWriter.NullProperty("endMark");
   }
 }
 
+void TextMarkerPayload::StreamPayload(SpliceableJSONWriter& aWriter,
+                                      const TimeStamp& aProcessStartTime,
+                                      UniqueStacks& aUniqueStacks) {
+  StreamCommonProps("Text", aWriter, aProcessStartTime, aUniqueStacks);
+  aWriter.StringProperty("name", mText.get());
+}
+
+void LogMarkerPayload::StreamPayload(SpliceableJSONWriter& aWriter,
+                                     const TimeStamp& aProcessStartTime,
+                                     UniqueStacks& aUniqueStacks) {
+  StreamCommonProps("Log", aWriter, aProcessStartTime, aUniqueStacks);
+  aWriter.StringProperty("name", mText.get());
+  aWriter.StringProperty("module", mModule.get());
+}
+
 void DOMEventMarkerPayload::StreamPayload(SpliceableJSONWriter& aWriter,
                                           const TimeStamp& aProcessStartTime,
                                           UniqueStacks& aUniqueStacks) {
   TracingMarkerPayload::StreamPayload(aWriter, aProcessStartTime,
                                       aUniqueStacks);
 
   WriteTime(aWriter, aProcessStartTime, mTimeStamp, "timeStamp");
   aWriter.StringProperty("eventType", NS_ConvertUTF16toUTF8(mEventType).get());
--- a/tools/profiler/public/ProfilerMarkerPayload.h
+++ b/tools/profiler/public/ProfilerMarkerPayload.h
@@ -6,16 +6,17 @@
 
 #ifndef ProfilerMarkerPayload_h
 #define ProfilerMarkerPayload_h
 
 #include "mozilla/Attributes.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/RefPtr.h"
 #include "mozilla/TimeStamp.h"
+#include "mozilla/UniquePtr.h"
 #include "mozilla/UniquePtrExtensions.h"
 #include "mozilla/net/TimingStruct.h"
 
 #include "nsString.h"
 #include "GeckoProfiler.h"
 
 #include "js/Utility.h"
 #include "gfxASurface.h"
@@ -354,9 +355,58 @@ class LongTaskMarkerPayload : public Pro
  public:
   LongTaskMarkerPayload(const mozilla::TimeStamp& aStartTime,
                         const mozilla::TimeStamp& aEndTime)
       : ProfilerMarkerPayload(aStartTime, aEndTime) {}
 
   DECL_STREAM_PAYLOAD
 };
 
+class TextMarkerPayload : public ProfilerMarkerPayload {
+ public:
+  TextMarkerPayload(const nsACString& aText,
+                    const mozilla::TimeStamp& aStartTime)
+      : ProfilerMarkerPayload(aStartTime, aStartTime), mText(aText) {}
+
+  TextMarkerPayload(const nsACString& aText,
+                    const mozilla::TimeStamp& aStartTime,
+                    const mozilla::TimeStamp& aEndTime)
+      : ProfilerMarkerPayload(aStartTime, aEndTime), mText(aText) {}
+
+  TextMarkerPayload(const nsACString& aText,
+                    const mozilla::TimeStamp& aStartTime,
+                    const mozilla::Maybe<nsID>& aDocShellId,
+                    const mozilla::Maybe<uint32_t>& aDocShellHistoryId)
+      : ProfilerMarkerPayload(aStartTime, aStartTime, aDocShellId,
+                              aDocShellHistoryId),
+        mText(aText) {}
+
+  TextMarkerPayload(const nsACString& aText,
+                    const mozilla::TimeStamp& aStartTime,
+                    const mozilla::TimeStamp& aEndTime,
+                    const mozilla::Maybe<nsID>& aDocShellId,
+                    const mozilla::Maybe<uint32_t>& aDocShellHistoryId)
+      : ProfilerMarkerPayload(aStartTime, aEndTime, aDocShellId,
+                              aDocShellHistoryId),
+        mText(aText) {}
+
+  DECL_STREAM_PAYLOAD
+
+ private:
+  nsCString mText;
+};
+
+class LogMarkerPayload : public ProfilerMarkerPayload {
+ public:
+  LogMarkerPayload(const char* aModule, const char* aText,
+                   const mozilla::TimeStamp& aStartTime)
+      : ProfilerMarkerPayload(aStartTime, aStartTime),
+        mModule(aModule),
+        mText(aText) {}
+
+  DECL_STREAM_PAYLOAD
+
+ private:
+  nsAutoCStringN<32> mModule;  // longest known LazyLogModule name is ~24
+  nsCString mText;
+};
+
 #endif  // ProfilerMarkerPayload_h
--- a/xpcom/base/Logging.cpp
+++ b/xpcom/base/Logging.cpp
@@ -17,16 +17,19 @@
 #include "mozilla/Sprintf.h"
 #include "mozilla/UniquePtrExtensions.h"
 #include "MainThreadUtils.h"
 #include "nsClassHashtable.h"
 #include "nsDebug.h"
 #include "nsDebugImpl.h"
 #include "NSPRLogModulesParser.h"
 #include "LogCommandLineHandler.h"
+#ifdef MOZ_GECKO_PROFILER
+#include "ProfilerMarkerPayload.h"
+#endif
 
 #include "prenv.h"
 #ifdef XP_WIN
 #include <process.h>
 #else
 #include <sys/types.h>
 #include <unistd.h>
 #endif
@@ -161,16 +164,17 @@ class LogModuleManager {
         mPrintEntryCount(0),
         mOutFile(nullptr),
         mToReleaseFile(nullptr),
         mOutFileNum(0),
         mOutFilePath(strdup("")),
         mMainThread(PR_GetCurrentThread()),
         mSetFromEnv(false),
         mAddTimestamp(false),
+        mAddProfilerMarker(false),
         mIsRaw(false),
         mIsSync(false),
         mRotate(0),
         mInitialized(false) {}
 
   ~LogModuleManager() {
     detail::LogFile* logFile = mOutFile.exchange(nullptr);
     delete logFile;
@@ -203,16 +207,17 @@ class LogModuleManager {
                                    // PR_SetEnv takes ownership of the string.
                                    PR_SetEnv(ToNewCString(env));
                                  });
 
     bool shouldAppend = false;
     bool addTimestamp = false;
     bool isSync = false;
     bool isRaw = false;
+    bool isMarkers = false;
     int32_t rotate = 0;
     const char* modules = PR_GetEnv("MOZ_LOG");
     if (!modules || !modules[0]) {
       modules = PR_GetEnv("MOZ_LOG_MODULES");
       if (modules) {
         NS_WARNING(
             "MOZ_LOG_MODULES is deprecated."
             "\nPlease use MOZ_LOG instead.");
@@ -225,39 +230,42 @@ class LogModuleManager {
             "NSPR_LOG_MODULES is deprecated."
             "\nPlease use MOZ_LOG instead.");
       }
     }
 
     // Need to capture `this` since `sLogModuleManager` is not set until after
     // initialization is complete.
     NSPRLogModulesParser(
-        modules,
-        [this, &shouldAppend, &addTimestamp, &isSync, &isRaw, &rotate](
-            const char* aName, LogLevel aLevel, int32_t aValue) mutable {
+        modules, [this, &shouldAppend, &addTimestamp, &isSync, &isRaw, &rotate,
+                  &isMarkers](const char* aName, LogLevel aLevel,
+                              int32_t aValue) mutable {
           if (strcmp(aName, "append") == 0) {
             shouldAppend = true;
           } else if (strcmp(aName, "timestamp") == 0) {
             addTimestamp = true;
           } else if (strcmp(aName, "sync") == 0) {
             isSync = true;
           } else if (strcmp(aName, "raw") == 0) {
             isRaw = true;
           } else if (strcmp(aName, "rotate") == 0) {
             rotate = (aValue << 20) / kRotateFilesNumber;
+          } else if (strcmp(aName, "profilermarkers") == 0) {
+            isMarkers = true;
           } else {
             this->CreateOrGetModule(aName)->SetLevel(aLevel);
           }
         });
 
     // Rotate implies timestamp to make the files readable
     mAddTimestamp = addTimestamp || rotate > 0;
     mIsSync = isSync;
     mIsRaw = isRaw;
     mRotate = rotate;
+    mAddProfilerMarker = isMarkers;
 
     if (rotate > 0 && shouldAppend) {
       NS_WARNING("MOZ_LOG: when you rotate the log, you cannot use append!");
     }
 
     const char* logFile = PR_GetEnv("MOZ_LOG_FILE");
     if (!logFile || !logFile[0]) {
       logFile = PR_GetEnv("NSPR_LOG_FILE");
@@ -396,16 +404,24 @@ class LogModuleManager {
       charsWritten = strlen(buff);
     } else if (static_cast<size_t>(charsWritten) >= kBuffSize - 1) {
       // We may have maxed out, allocate a buffer instead.
       allocatedBuff = mozilla::Vsmprintf(aFmt, aArgs);
       buffToWrite = allocatedBuff.get();
       charsWritten = strlen(buffToWrite);
     }
 
+#ifdef MOZ_GECKO_PROFILER
+    if (mAddProfilerMarker && profiler_is_active()) {
+      profiler_add_marker(
+          "LogMessages",
+          MakeUnique<LogMarkerPayload>(aName, buffToWrite, TimeStamp::Now()));
+    }
+#endif
+
     // Determine if a newline needs to be appended to the message.
     const char* newline = "";
     if (charsWritten == 0 || buffToWrite[charsWritten - 1] != '\n') {
       newline = "\n";
     }
 
     FILE* out = stderr;
 
@@ -519,16 +535,17 @@ class LogModuleManager {
   // is atomic regardless ordering.
   Atomic<uint32_t, Relaxed> mOutFileNum;
   // Just keeps the actual file path for further use.
   UniqueFreePtr<char[]> mOutFilePath;
 
   PRThread* mMainThread;
   bool mSetFromEnv;
   Atomic<bool, Relaxed> mAddTimestamp;
+  Atomic<bool, Relaxed> mAddProfilerMarker;
   Atomic<bool, Relaxed> mIsRaw;
   Atomic<bool, Relaxed> mIsSync;
   int32_t mRotate;
   bool mInitialized;
 };
 
 StaticAutoPtr<LogModuleManager> sLogModuleManager;