Merge mozilla central to inbound. r=merge a=merge on a CLOSED TREE
authorBogdan Tara <btara@mozilla.com>
Tue, 09 Jan 2018 00:18:26 +0200
changeset 452594 a0ab006eb17ab9b30db5ca88a85f3d2fba6ab97a
parent 452593 3d72692873fab2af04187e740fdc21583ec336e5 (current diff)
parent 452534 05fed903f40f05fd923ba2137696ecc1fa0bafe6 (diff)
child 452595 781a6bc83dde7326adbfbae640041cc8a451a565
push id1648
push usermtabara@mozilla.com
push dateThu, 01 Mar 2018 12:45:47 +0000
treeherdermozilla-release@cbb9688c2eeb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge, merge
milestone59.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla central to inbound. r=merge a=merge on a CLOSED TREE
toolkit/components/telemetry/Histograms.json
--- a/browser/base/content/newtab/page.js
+++ b/browser/base/content/newtab/page.js
@@ -175,21 +175,16 @@ var gPage = {
     }
   },
 
   /**
    * Handles unload event
    */
   _handleUnloadEvent: function Page_handleUnloadEvent() {
     gAllPages.unregister(this);
-    // compute page life-span and send telemetry probe: using milli-seconds will leave
-    // many low buckets empty. Instead we use half-second precision to make low end
-    // of histogram linear and not lose the change in user attention
-    let delta = Math.round((Date.now() - this._firstVisibleTime) / 500);
-    Services.telemetry.getHistogramById("NEWTAB_PAGE_LIFE_SPAN").add(delta);
   },
 
   /**
    * Handles all page events.
    */
   handleEvent: function Page_handleEvent(aEvent) {
     switch (aEvent.type) {
       case "load":
@@ -228,28 +223,22 @@ var gPage = {
 
         setTimeout(() => this.onPageFirstVisible());
         removeEventListener("visibilitychange", this);
         break;
     }
   },
 
   onPageFirstVisible: function () {
-    // Record another page impression.
-    Services.telemetry.getHistogramById("NEWTAB_PAGE_SHOWN").add(true);
-
     for (let site of gGrid.sites) {
       if (site) {
         site.captureIfMissing();
       }
     }
 
-    // save timestamp to compute page life-span delta
-    this._firstVisibleTime = Date.now();
-
     if (document.readyState == "complete") {
       this.onPageVisibleAndLoaded();
     } else {
       addEventListener("load", this);
     }
   },
 
   onPageVisibleAndLoaded() {
--- a/browser/base/content/newtab/sites.js
+++ b/browser/base/content/newtab/sites.js
@@ -238,48 +238,25 @@ Site.prototype = {
       let originAttributes = document.docShell.getOriginAttributes();
       let principal = Services.scriptSecurityManager
                               .createCodebasePrincipal(uri, originAttributes);
       sc.speculativeConnect2(uri, principal, null);
     } catch (e) {}
   },
 
   /**
-   * Record interaction with site using telemetry.
-   */
-  _recordSiteClicked: function Site_recordSiteClicked(aIndex) {
-    if (Services.prefs.prefHasUserValue("browser.newtabpage.rows") ||
-        Services.prefs.prefHasUserValue("browser.newtabpage.columns") ||
-        aIndex > 8) {
-      // We only want to get indices for the default configuration, everything
-      // else goes in the same bucket.
-      aIndex = 9;
-    }
-    Services.telemetry.getHistogramById("NEWTAB_PAGE_SITE_CLICKED")
-                      .add(aIndex);
-  },
-
-  /**
    * Handles site click events.
    */
   onClick: function Site_onClick(aEvent) {
     let pinned = this.isPinned();
     let tileIndex = this.cell.index;
     let {button, target} = aEvent;
 
-    // Handle tile/thumbnail link click
-    if (target.classList.contains("newtab-link") ||
-        target.parentElement.classList.contains("newtab-link")) {
-      // Record for primary and middle clicks
-      if (button == 0 || button == 1) {
-        this._recordSiteClicked(tileIndex);
-      }
-    }
     // Only handle primary clicks for the remaining targets
-    else if (button == 0) {
+    if (button == 0) {
       aEvent.preventDefault();
       if (target.classList.contains("newtab-control-block")) {
         this.block();
       }
       else if (pinned && target.classList.contains("newtab-control-pin")) {
         this.unpin();
       }
       else if (!pinned && target.classList.contains("newtab-control-pin")) {
--- a/browser/extensions/formautofill/FormAutofillHeuristics.jsm
+++ b/browser/extensions/formautofill/FormAutofillHeuristics.jsm
@@ -529,27 +529,66 @@ this.FormAutofillHeuristics = {
    * @param {FieldScanner} fieldScanner
    *        The current parsing status for all elements
    * @returns {boolean}
    *          Return true if there is any field can be recognized in the parser,
    *          otherwise false.
    */
   _parseAddressFields(fieldScanner) {
     let parsedFields = false;
-    let addressLines = ["address-line1", "address-line2", "address-line3"];
-    for (let i = 0; !fieldScanner.parsingFinished && i < addressLines.length; i++) {
+    const addressLines = ["address-line1", "address-line2", "address-line3"];
+
+    // TODO: These address-line* regexps are for the lines with numbers, and
+    // they are the subset of the regexps in `heuristicsRegexp.js`. We have to
+    // find a better way to make them consistent.
+    const addressLineRegexps = {
+      "address-line1": new RegExp(
+        "address[_-]?line(1|one)|address1|addr1" +
+        "|addrline1|address_1" + // Extra rules by Firefox
+        "|indirizzo1" + // it-IT
+        "|住所1" + // ja-JP
+        "|地址1" + // zh-CN
+        "|주소.?1", // ko-KR
+        "iu"
+      ),
+      "address-line2": new RegExp(
+        "address[_-]?line(2|two)|address2|addr2" +
+        "|addrline2|address_2" + // Extra rules by Firefox
+        "|indirizzo2" + // it-IT
+        "|住所2" + // ja-JP
+        "|地址2" + // zh-CN
+        "|주소.?2", // ko-KR
+        "iu"
+      ),
+      "address-line3": new RegExp(
+        "address[_-]?line(3|three)|address3|addr3" +
+        "|addrline3|address_3" + // Extra rules by Firefox
+        "|indirizzo3" + // it-IT
+        "|住所3" + // ja-JP
+        "|地址3" + // zh-CN
+        "|주소.?3", // ko-KR
+        "iu"
+      ),
+    };
+    while (!fieldScanner.parsingFinished) {
       let detail = fieldScanner.getFieldDetailByIndex(fieldScanner.parsingIndex);
-      if (!detail || !addressLines.includes(detail.fieldName)) {
-        // When the field is not related to any address-line[1-3] fields, it
-        // means the parsing process can be terminated.
+      if (!detail || !addressLines.includes(detail.fieldName) || detail._reason == "autocomplete") {
+        // When the field is not related to any address-line[1-3] fields or
+        // determined by autocomplete attr, it means the parsing process can be
+        // terminated.
         break;
       }
-      fieldScanner.updateFieldName(fieldScanner.parsingIndex, addressLines[i]);
+      const elem = detail.elementWeakRef.get();
+      for (let regexp of Object.keys(addressLineRegexps)) {
+        if (this._matchRegexp(elem, addressLineRegexps[regexp])) {
+          fieldScanner.updateFieldName(fieldScanner.parsingIndex, regexp);
+          parsedFields = true;
+        }
+      }
       fieldScanner.parsingIndex++;
-      parsedFields = true;
     }
 
     return parsedFields;
   },
 
   /**
    * Try to look for expiration date fields and revise the field names if needed.
    *
--- a/browser/extensions/formautofill/content/heuristicsRegexp.js
+++ b/browser/extensions/formautofill/content/heuristicsRegexp.js
@@ -61,44 +61,47 @@ var HeuristicsRegExp = {
       "iu"
     ),
     "street-address": new RegExp(
       "streetaddress|street-address",
       "iu"
     ),
     "address-line1": new RegExp(
       "^address$|address[_-]?line(one)?|address1|addr1|street" +
+      "|addrline1|address_1" + // Extra rules by Firefox
       "|(?:shipping|billing)address$" +
       "|strasse|straße|hausnummer|housenumber" + // de-DE
       "|house.?name" + // en-GB
       "|direccion|dirección" + // es
       "|adresse" + // fr-FR
       "|indirizzo" + // it-IT
       "|^住所$|住所1" + // ja-JP
       "|morada|endereço" + // pt-BR, pt-PT
       "|Адрес" + // ru
       "|地址" + // zh-CN
       "|^주소.?$|주소.?1", // ko-KR
       "iu"
     ),
     "address-line2": new RegExp(
       "address[_-]?line(2|two)|address2|addr2|street|suite|unit" +
+      "|addrline2|address_2" + // Extra rules by Firefox
       "|adresszusatz|ergänzende.?angaben" + // de-DE
       "|direccion2|colonia|adicional" + // es
       "|addresssuppl|complementnom|appartement" + // fr-FR
       "|indirizzo2" + // it-IT
       "|住所2" + // ja-JP
       "|complemento|addrcomplement" + // pt-BR, pt-PT
       "|Улица" + // ru
       "|地址2" + // zh-CN
       "|주소.?2", // ko-KR
       "iu"
     ),
     "address-line3": new RegExp(
       "address[_-]?line(3|three)|address3|addr3|street|suite|unit" +
+      "|addrline3|address_3" + // Extra rules by Firefox
       "|adresszusatz|ergänzende.?angaben" + // de-DE
       "|direccion3|colonia|adicional" + // es
       "|addresssuppl|complementnom|appartement" + // fr-FR
       "|indirizzo3" + // it-IT
       "|住所3" + // ja-JP
       "|complemento|addrcomplement" + // pt-BR, pt-PT
       "|Улица" + // ru
       "|地址3" + // zh-CN
--- a/browser/extensions/formautofill/test/fixtures/autocomplete_basic.html
+++ b/browser/extensions/formautofill/test/fixtures/autocomplete_basic.html
@@ -35,11 +35,19 @@
     <p><label>cc-name <input type="text" id="B_cc-name" autocomplete="cc-name" /></label></p>
     <p><label>cc-exp-month <input type="text" id="B_cc-exp-month" autocomplete="cc-exp-month" /></label></p>
     <p><label>cc-exp-year <input type="text" id="B_cc-exp-year" autocomplete="cc-exp-year" /></label></p>
     <hr>
     <p><input type="submit" /></p>
     <p><button type="reset">Reset</button></p>
   </form>
 
+  <form id="formC">
+    <p><label><input type="text" name="someprefixAddrLine1" /></label></p>
+    <p><label>City: <input type="text" name="address-level2" /></label></p>
+    <p><label><input type="text" name="someprefixAddrLine2" /></label></p>
+    <p><label>Organization: <input type="text" name="organization" /></label></p>
+    <p><label><input type="text" name="someprefixAddrLine3" /></label></p>
+  </form>
+
 </body>
 </html>
 
--- a/browser/extensions/formautofill/test/unit/heuristics/test_basic.js
+++ b/browser/extensions/formautofill/test/unit/heuristics/test_basic.js
@@ -27,12 +27,19 @@ runHeuristicsTest([
         {"section": "", "addressType": "", "contactType": "", "fieldName": "country"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "tel"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "email"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-number"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-name"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-exp-month"},
         {"section": "", "addressType": "", "contactType": "", "fieldName": "cc-exp-year"},
       ]],
+      [[
+        {"section": "", "addressType": "", "contactType": "", "fieldName": "address-line1"},
+        {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2"},
+        {"section": "", "addressType": "", "contactType": "", "fieldName": "address-line2"},
+        {"section": "", "addressType": "", "contactType": "", "fieldName": "organization"},
+        {"section": "", "addressType": "", "contactType": "", "fieldName": "address-line3"},
+      ]],
     ],
   },
 ], "../../fixtures/");
 
--- a/browser/extensions/formautofill/test/unit/test_getAdaptedProfiles.js
+++ b/browser/extensions/formautofill/test/unit/test_getAdaptedProfiles.js
@@ -115,29 +115,56 @@ const TESTCASES = [
       "address-line3": "line3",
       "address-level1": "CA",
       "country": "US",
       "tel": "+19876543210",
       "tel-national": "9876543210",
     }],
   },
   {
-    description: "Address form with street-address, address-line[1, 3]",
+    description: "Address form with street-address, address-line[1, 3]" +
+                 ", determined by autocomplete attr",
     document: `<form>
                <input id="street-addr" autocomplete="street-address">
                <input id="line1" autocomplete="address-line1">
                <input id="line3" autocomplete="address-line3">
                </form>`,
     profileData: [Object.assign({}, DEFAULT_ADDRESS_RECORD)],
     expectedResult: [{
       "guid": "123",
       "street-address": "2 Harrison St line2 line3",
       "-moz-street-address-one-line": "2 Harrison St line2 line3",
-      "address-line1": "2 Harrison St",
-      "address-line2": "line2 line3",
+      // Since the form is missing address-line2 field, the value of
+      // address-line1 should contain line2 value as well.
+      "address-line1": "2 Harrison St line2",
+      "address-line2": "line2",
+      "address-line3": "line3",
+      "address-level1": "CA",
+      "country": "US",
+      "tel": "+19876543210",
+      "tel-national": "9876543210",
+    }],
+  },
+  {
+    description: "Address form with street-address, address-line[1, 3]" +
+                 ", determined by heuristics",
+    document: `<form>
+               <input id="street-address">
+               <input id="address-line1">
+               <input id="address-line3">
+               </form>`,
+    profileData: [Object.assign({}, DEFAULT_ADDRESS_RECORD)],
+    expectedResult: [{
+      "guid": "123",
+      "street-address": "2 Harrison St line2 line3",
+      "-moz-street-address-one-line": "2 Harrison St line2 line3",
+      // Since the form is missing address-line2 field, the value of
+      // address-line1 should contain line2 value as well.
+      "address-line1": "2 Harrison St line2",
+      "address-line2": "line2",
       "address-line3": "line3",
       "address-level1": "CA",
       "country": "US",
       "tel": "+19876543210",
       "tel-national": "9876543210",
     }],
   },
   {
--- a/build/unix/elfhack/elfhack.cpp
+++ b/build/unix/elfhack/elfhack.cpp
@@ -144,48 +144,48 @@ public:
         entry_point = sym->value.getValue();
 
         // Get all relevant sections from the injected code object.
         add_code_section(sym->value.getSection());
 
         // Adjust code sections offsets according to their size
         std::vector<ElfSection *>::iterator c = code.begin();
         (*c)->getShdr().sh_addr = 0;
-        for(ElfSection *last = *(c++); c != code.end(); c++) {
+        for(ElfSection *last = *(c++); c != code.end(); ++c) {
             unsigned int addr = last->getShdr().sh_addr + last->getSize();
             if (addr & ((*c)->getAddrAlign() - 1))
                 addr = (addr | ((*c)->getAddrAlign() - 1)) + 1;
             (*c)->getShdr().sh_addr = addr;
             // We need to align this section depending on the greater
             // alignment required by code sections.
             if (shdr.sh_addralign < (*c)->getAddrAlign())
                 shdr.sh_addralign = (*c)->getAddrAlign();
         }
         shdr.sh_size = code.back()->getAddr() + code.back()->getSize();
         data = new char[shdr.sh_size];
         char *buf = data;
-        for (c = code.begin(); c != code.end(); c++) {
+        for (c = code.begin(); c != code.end(); ++c) {
             memcpy(buf, (*c)->getData(), (*c)->getSize());
             buf += (*c)->getSize();
         }
         name = elfhack_text;
     }
 
     ~ElfRelHackCode_Section() {
         delete elf;
     }
 
     void serialize(std::ofstream &file, char ei_class, char ei_data)
     {
         // Readjust code offsets
-        for (std::vector<ElfSection *>::iterator c = code.begin(); c != code.end(); c++)
+        for (std::vector<ElfSection *>::iterator c = code.begin(); c != code.end(); ++c)
             (*c)->getShdr().sh_addr += getAddr();
 
         // Apply relocations
-        for (std::vector<ElfSection *>::iterator c = code.begin(); c != code.end(); c++) {
+        for (std::vector<ElfSection *>::iterator c = code.begin(); c != code.end(); ++c) {
             for (ElfSection *rel = elf->getSection(1); rel != nullptr; rel = rel->getNext())
                 if (((rel->getType() == SHT_REL) ||
                      (rel->getType() == SHT_RELA)) &&
                     (rel->getInfo().section == *c)) {
                     if (rel->getType() == SHT_REL)
                         apply_relocations((ElfRel_Section<Elf_Rel> *)rel, *c);
                     else
                         apply_relocations((ElfRel_Section<Elf_Rela> *)rel, *c);
@@ -232,17 +232,17 @@ private:
             }
         }
     }
 
     template <typename Rel_Type>
     void scan_relocs_for_code(ElfRel_Section<Rel_Type> *rel)
     {
         ElfSymtab_Section *symtab = (ElfSymtab_Section *)rel->getLink();
-        for (auto r = rel->rels.begin(); r != rel->rels.end(); r++) {
+        for (auto r = rel->rels.begin(); r != rel->rels.end(); ++r) {
             ElfSection *section = symtab->syms[ELF32_R_SYM(r->r_info)].value.getSection();
             add_code_section(section);
         }
     }
 
     class pc32_relocation {
     public:
         Elf32_Addr operator()(unsigned int base_addr, Elf32_Off offset,
@@ -345,17 +345,17 @@ private:
 
     template <typename Rel_Type>
     void apply_relocations(ElfRel_Section<Rel_Type> *rel, ElfSection *the_code)
     {
         assert(rel->getType() == Rel_Type::sh_type);
         char *buf = data + (the_code->getAddr() - code.front()->getAddr());
         // TODO: various checks on the sections
         ElfSymtab_Section *symtab = (ElfSymtab_Section *)rel->getLink();
-        for (typename std::vector<Rel_Type>::iterator r = rel->rels.begin(); r != rel->rels.end(); r++) {
+        for (typename std::vector<Rel_Type>::iterator r = rel->rels.begin(); r != rel->rels.end(); ++r) {
             // TODO: various checks on the symbol
             const char *name = symtab->syms[ELF32_R_SYM(r->r_info)].name;
             unsigned int addr;
             if (symtab->syms[ELF32_R_SYM(r->r_info)].value.getSection() == nullptr) {
                 if (strcmp(name, "relhack") == 0) {
                     addr = relhack_section.getAddr();
                 } else if (strcmp(name, "elf_header") == 0) {
                     // TODO: change this ungly hack to something better
@@ -471,17 +471,17 @@ void maybe_split_segment(Elf *elf, ElfSe
             phdr.p_filesz = (unsigned int)-1;
             phdr.p_memsz = (unsigned int)-1;
             ElfSegment *newSegment = new ElfSegment(&phdr);
             elf->insertSegmentAfter(segment, newSegment);
             ElfSection *section = *it;
             for (; it != segment->end(); ++it) {
                 newSegment->addSection(*it);
             }
-            for (it = newSegment->begin(); it != newSegment->end(); it++) {
+            for (it = newSegment->begin(); it != newSegment->end(); ++it) {
                 segment->removeSection(*it);
             }
             // Fill the virtual address space gap left between the two PT_LOADs
             // with a new PT_LOAD with no permissions. This avoids the linker
             // (especially bionic's) filling the gap with anonymous memory,
             // which breakpad doesn't like.
             // /!\ running strip on a elfhacked binary will break this filler
             // PT_LOAD.
@@ -571,17 +571,17 @@ int do_relocation_section(Elf *elf, unsi
     ElfSymtab_Section *symtab = (ElfSymtab_Section *) section->getLink();
     Elf_SymValue *sym = symtab->lookup("__cxa_pure_virtual");
 
     std::vector<Rel_Type> new_rels;
     Elf_RelHack relhack_entry;
     relhack_entry.r_offset = relhack_entry.r_info = 0;
     size_t init_array_reloc = 0;
     for (typename std::vector<Rel_Type>::iterator i = section->rels.begin();
-         i != section->rels.end(); i++) {
+         i != section->rels.end(); ++i) {
         // We don't need to keep R_*_NONE relocations
         if (!ELF32_R_TYPE(i->r_info))
             continue;
         ElfLocation loc(i->r_offset, elf);
         // __cxa_pure_virtual is a function used in vtables to point at pure
         // virtual methods. The __cxa_pure_virtual function usually abort()s.
         // These functions are however normally never called. In the case
         // where they would, jumping to the null address instead of calling
--- a/devtools/client/canvasdebugger/callslist.js
+++ b/devtools/client/canvasdebugger/callslist.js
@@ -4,17 +4,17 @@
 /* import-globals-from canvasdebugger.js */
 /* globals window, document */
 "use strict";
 
 /**
  * Functions handling details about a single recorded animation frame snapshot
  * (the calls list, rendering preview, thumbnails filmstrip etc.).
  */
-var CallsListView = Heritage.extend(WidgetMethods, {
+var CallsListView = extend(WidgetMethods, {
   /**
    * Initialization function, called when the tool is started.
    */
   initialize: function () {
     this.widget = new SideMenuWidget($("#calls-list"));
     this._slider = $("#calls-slider");
     this._searchbox = $("#calls-searchbox");
     this._filmstrip = $("#snapshot-filmstrip");
--- a/devtools/client/canvasdebugger/canvasdebugger.js
+++ b/devtools/client/canvasdebugger/canvasdebugger.js
@@ -9,20 +9,21 @@ const { require } = Cu.import("resource:
 const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
 const { SideMenuWidget } = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
 const promise = require("promise");
 const Services = require("Services");
 const EventEmitter = require("devtools/shared/old-event-emitter");
 const { CallWatcherFront } = require("devtools/shared/fronts/call-watcher");
 const { CanvasFront } = require("devtools/shared/fronts/canvas");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const { extend } = require("devtools/shared/extend");
 const flags = require("devtools/shared/flags");
 const { LocalizationHelper } = require("devtools/shared/l10n");
 const { PluralForm } = require("devtools/shared/plural-form");
-const { Heritage, WidgetMethods, setNamedTimeout, clearNamedTimeout,
+const { WidgetMethods, setNamedTimeout, clearNamedTimeout,
         setConditionalTimeout } = require("devtools/client/shared/widgets/view-helpers");
 
 // Use privileged promise in panel documents to prevent having them to freeze
 // during toolbox destruction. See bug 1402779.
 const Promise = require("Promise");
 
 const CANVAS_ACTOR_RECORDING_ATTEMPT = flags.testing ? 500 : 5000;
 
--- a/devtools/client/canvasdebugger/snapshotslist.js
+++ b/devtools/client/canvasdebugger/snapshotslist.js
@@ -3,17 +3,17 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 /* import-globals-from canvasdebugger.js */
 /* globals window, document */
 "use strict";
 
 /**
  * Functions handling the recorded animation frame snapshots UI.
  */
-var SnapshotsListView = Heritage.extend(WidgetMethods, {
+var SnapshotsListView = extend(WidgetMethods, {
   /**
    * Initialization function, called when the tool is started.
    */
   initialize: function () {
     this.widget = new SideMenuWidget($("#snapshots-list"), {
       showArrows: true
     });
 
--- a/devtools/client/debugger/content/views/event-listeners-view.js
+++ b/devtools/client/debugger/content/views/event-listeners-view.js
@@ -3,17 +3,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 /* import-globals-from ../../debugger-controller.js */
 
 const actions = require("../actions/event-listeners");
 const { bindActionCreators } = require("devtools/client/shared/vendor/redux");
-const { Heritage, WidgetMethods } = require("devtools/client/shared/widgets/view-helpers");
+const { extend } = require("devtools/shared/extend");
+const { WidgetMethods } = require("devtools/client/shared/widgets/view-helpers");
 const { SideMenuWidget } = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
 
 /**
  * Functions handling the event listeners UI.
  */
 function EventListenersView(controller) {
   dumpn("EventListenersView was instantiated");
 
@@ -21,17 +22,17 @@ function EventListenersView(controller) 
   this.getState = () => controller.getState().eventListeners;
 
   this._onCheck = this._onCheck.bind(this);
   this._onClick = this._onClick.bind(this);
 
   controller.onChange("event-listeners", this.renderListeners.bind(this));
 }
 
-EventListenersView.prototype = Heritage.extend(WidgetMethods, {
+EventListenersView.prototype = extend(WidgetMethods, {
   /**
    * Initialization function, called when the debugger is started.
    */
   initialize: function () {
     dumpn("Initializing the EventListenersView");
 
     this.widget = new SideMenuWidget(document.getElementById("event-listeners"), {
       showItemCheckboxes: true,
--- a/devtools/client/debugger/content/views/sources-view.js
+++ b/devtools/client/debugger/content/views/sources-view.js
@@ -15,18 +15,18 @@ const {
   makeLocationId
 } = require("../queries");
 const actions = Object.assign(
   {},
   require("../actions/sources"),
   require("../actions/breakpoints")
 );
 const { bindActionCreators } = require("devtools/client/shared/vendor/redux");
+const { extend } = require("devtools/shared/extend");
 const {
-  Heritage,
   WidgetMethods,
   setNamedTimeout
 } = require("devtools/client/shared/widgets/view-helpers");
 const { Task } = require("devtools/shared/task");
 const { SideMenuWidget } = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
 const { gDevTools } = require("devtools/client/framework/devtools");
 const {KeyCodes} = require("devtools/client/shared/keycodes");
 
@@ -75,17 +75,17 @@ function SourcesView(controller, Debugge
   this._onConditionalPopupHiding = this._onConditionalPopupHiding.bind(this);
   this._onConditionalTextboxKeyPress = this._onConditionalTextboxKeyPress.bind(this);
   this._onEditorContextMenuOpen = this._onEditorContextMenuOpen.bind(this);
   this._onCopyUrlCommand = this._onCopyUrlCommand.bind(this);
   this._onNewTabCommand = this._onNewTabCommand.bind(this);
   this._onConditionalPopupHidden = this._onConditionalPopupHidden.bind(this);
 }
 
-SourcesView.prototype = Heritage.extend(WidgetMethods, {
+SourcesView.prototype = extend(WidgetMethods, {
   /**
    * Initialization function, called when the debugger is started.
    */
   initialize: function (isWorker) {
     dumpn("Initializing the SourcesView");
 
     this.widget = new SideMenuWidget(document.getElementById("sources"), {
       contextMenu: document.getElementById("debuggerSourcesContextMenu"),
@@ -345,23 +345,23 @@ SourcesView.prototype = Heritage.extend(
 
     // Get the source item to which the breakpoint should be attached.
     let sourceItem = this.getItemByValue(location.actor);
     if (!sourceItem) {
       return;
     }
 
     // Create the element node and menu popup for the breakpoint item.
-    let breakpointArgs = Heritage.extend(breakpoint.asMutable(), options);
+    let breakpointArgs = extend(breakpoint.asMutable(), options);
     let breakpointView = this._createBreakpointView.call(this, breakpointArgs);
     let contextMenu = this._createContextMenu.call(this, breakpointArgs);
 
     // Append a breakpoint child item to the corresponding source item.
     sourceItem.append(breakpointView.container, {
-      attachment: Heritage.extend(breakpointArgs, {
+      attachment: extend(breakpointArgs, {
         actor: location.actor,
         line: location.line,
         view: breakpointView,
         popup: contextMenu
       }),
       attributes: [
         ["contextmenu", contextMenu.menupopupId]
       ],
--- a/devtools/client/debugger/debugger-controller.js
+++ b/devtools/client/debugger/debugger-controller.js
@@ -104,18 +104,19 @@ const { require } = BrowserLoader({
 const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineConstant(this, "require", require);
 const { SimpleListWidget } = require("resource://devtools/client/shared/widgets/SimpleListWidget.jsm");
 const { BreadcrumbsWidget } = require("resource://devtools/client/shared/widgets/BreadcrumbsWidget.jsm");
 const { SideMenuWidget } = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
 const { VariablesView } = require("resource://devtools/client/shared/widgets/VariablesView.jsm");
 const { VariablesViewController, StackFrameUtils } = require("resource://devtools/client/shared/widgets/VariablesViewController.jsm");
 const EventEmitter = require("devtools/shared/old-event-emitter");
+const { extend } = require("devtools/shared/extend");
 const { gDevTools } = require("devtools/client/framework/devtools");
-const { ViewHelpers, Heritage, WidgetMethods, setNamedTimeout,
+const { ViewHelpers, WidgetMethods, setNamedTimeout,
         clearNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
 
 // Use privileged promise in panel documents to prevent having them to freeze
 // during toolbox destruction. See bug 1402779.
 const Promise = require("Promise");
 
 // React
 const React = require("devtools/client/shared/vendor/react");
--- a/devtools/client/debugger/debugger-view.js
+++ b/devtools/client/debugger/debugger-view.js
@@ -841,17 +841,17 @@ var DebuggerView = {
 
 /**
  * A custom items container, used for displaying views like the
  * FilteredSources, FilteredFunctions etc., inheriting the generic WidgetMethods.
  */
 function ResultsPanelContainer() {
 }
 
-ResultsPanelContainer.prototype = Heritage.extend(WidgetMethods, {
+ResultsPanelContainer.prototype = extend(WidgetMethods, {
   /**
    * Sets the anchor node for this container panel.
    * @param nsIDOMNode aNode
    */
   set anchor(aNode) {
     this._anchor = aNode;
 
     // If the anchor node is not null, create a panel to attach to the anchor
--- a/devtools/client/debugger/views/filter-view.js
+++ b/devtools/client/debugger/views/filter-view.js
@@ -546,17 +546,17 @@ function FilteredSourcesView(DebuggerVie
   dumpn("FilteredSourcesView was instantiated");
 
   this.DebuggerView = DebuggerView;
 
   this._onClick = this._onClick.bind(this);
   this._onSelect = this._onSelect.bind(this);
 }
 
-FilteredSourcesView.prototype = Heritage.extend(ResultsPanelContainer.prototype, {
+FilteredSourcesView.prototype = extend(ResultsPanelContainer.prototype, {
   /**
    * Initialization function, called when the debugger is started.
    */
   initialize: function () {
     dumpn("Initializing the FilteredSourcesView");
 
     this.anchor = document.getElementById("searchbox");
     this.widget.addEventListener("select", this._onSelect);
@@ -709,17 +709,17 @@ function FilteredFunctionsView(SourceScr
   this.SourceScripts = SourceScripts;
   this.Parser = Parser;
   this.DebuggerView = DebuggerView;
 
   this._onClick = this._onClick.bind(this);
   this._onSelect = this._onSelect.bind(this);
 }
 
-FilteredFunctionsView.prototype = Heritage.extend(ResultsPanelContainer.prototype, {
+FilteredFunctionsView.prototype = extend(ResultsPanelContainer.prototype, {
   /**
    * Initialization function, called when the debugger is started.
    */
   initialize: function () {
     dumpn("Initializing the FilteredFunctionsView");
 
     this.anchor = document.getElementById("searchbox");
     this.widget.addEventListener("select", this._onSelect);
--- a/devtools/client/debugger/views/global-search-view.js
+++ b/devtools/client/debugger/views/global-search-view.js
@@ -18,17 +18,17 @@ function GlobalSearchView(DebuggerContro
   this.SourceScripts = DebuggerController.SourceScripts;
   this.DebuggerView = DebuggerView;
 
   this._onHeaderClick = this._onHeaderClick.bind(this);
   this._onLineClick = this._onLineClick.bind(this);
   this._onMatchClick = this._onMatchClick.bind(this);
 }
 
-GlobalSearchView.prototype = Heritage.extend(WidgetMethods, {
+GlobalSearchView.prototype = extend(WidgetMethods, {
   /**
    * Initialization function, called when the debugger is started.
    */
   initialize: function () {
     dumpn("Initializing the GlobalSearchView");
 
     this.widget = new SimpleListWidget(document.getElementById("globalsearch"));
     this._splitter = document.querySelector("#globalsearch + .devtools-horizontal-splitter");
--- a/devtools/client/debugger/views/stack-frames-classic-view.js
+++ b/devtools/client/debugger/views/stack-frames-classic-view.js
@@ -15,17 +15,17 @@
  */
 function StackFramesClassicListView(DebuggerController, DebuggerView) {
   dumpn("StackFramesClassicListView was instantiated");
 
   this.DebuggerView = DebuggerView;
   this._onSelect = this._onSelect.bind(this);
 }
 
-StackFramesClassicListView.prototype = Heritage.extend(WidgetMethods, {
+StackFramesClassicListView.prototype = extend(WidgetMethods, {
   /**
    * Initialization function, called when the debugger is started.
    */
   initialize: function () {
     dumpn("Initializing the StackFramesClassicListView");
 
     this.widget = new SideMenuWidget(document.getElementById("callstack-list"));
     this.widget.addEventListener("select", this._onSelect);
--- a/devtools/client/debugger/views/stack-frames-view.js
+++ b/devtools/client/debugger/views/stack-frames-view.js
@@ -20,17 +20,17 @@ function StackFramesView(DebuggerControl
 
   this._onStackframeRemoved = this._onStackframeRemoved.bind(this);
   this._onSelect = this._onSelect.bind(this);
   this._onScroll = this._onScroll.bind(this);
   this._afterScroll = this._afterScroll.bind(this);
   this._getStackAsString = this._getStackAsString.bind(this);
 }
 
-StackFramesView.prototype = Heritage.extend(WidgetMethods, {
+StackFramesView.prototype = extend(WidgetMethods, {
   /**
    * Initialization function, called when the debugger is started.
    */
   initialize: function () {
     dumpn("Initializing the StackFramesView");
 
     this._popupset = document.getElementById("debuggerPopupset");
 
--- a/devtools/client/debugger/views/watch-expressions-view.js
+++ b/devtools/client/debugger/views/watch-expressions-view.js
@@ -24,17 +24,17 @@ function WatchExpressionsView(DebuggerCo
   this.deleteExpression = this.deleteExpression.bind(this);
   this._createItemView = this._createItemView.bind(this);
   this._onClick = this._onClick.bind(this);
   this._onClose = this._onClose.bind(this);
   this._onBlur = this._onBlur.bind(this);
   this._onKeyPress = this._onKeyPress.bind(this);
 }
 
-WatchExpressionsView.prototype = Heritage.extend(WidgetMethods, {
+WatchExpressionsView.prototype = extend(WidgetMethods, {
   /**
    * Initialization function, called when the debugger is started.
    */
   initialize: function () {
     dumpn("Initializing the WatchExpressionsView");
 
     this.widget = new SimpleListWidget(document.getElementById("expressions"));
     this.widget.setAttribute("context", "debuggerWatchExpressionsContextMenu");
--- a/devtools/client/debugger/views/workers-view.js
+++ b/devtools/client/debugger/views/workers-view.js
@@ -8,17 +8,17 @@
 /* import-globals-from ../utils.js */
 /* globals document */
 "use strict";
 
 function WorkersView() {
   this._onWorkerSelect = this._onWorkerSelect.bind(this);
 }
 
-WorkersView.prototype = Heritage.extend(WidgetMethods, {
+WorkersView.prototype = extend(WidgetMethods, {
   initialize: function () {
     if (!Prefs.workersEnabled) {
       return;
     }
 
     document.getElementById("workers-pane").removeAttribute("hidden");
     document.getElementById("workers-splitter").removeAttribute("hidden");
 
--- a/devtools/client/netmonitor/src/assets/styles/NetworkDetailsPanel.css
+++ b/devtools/client/netmonitor/src/assets/styles/NetworkDetailsPanel.css
@@ -154,16 +154,17 @@
 /* If there is a source editor shows up in the last row of TreeView,
  * it should occupy the available vertical space.
  */
 .network-monitor .tree-container .treeTable .editor-row-container,
 .network-monitor .tree-container .treeTable tr:last-child td[colspan="2"] {
   display: block;
   height: 100%;
   flex: 1;
+  overflow-x: auto;
 }
 
 .network-monitor .source-editor-mount {
   width: 100%;
   height: 100%;
 }
 
 .network-monitor .headers-summary-label,
--- a/devtools/client/netmonitor/test/browser_net_post-data-01.js
+++ b/devtools/client/netmonitor/test/browser_net_post-data-01.js
@@ -6,17 +6,17 @@
 /**
  * Tests if the POST requests display the correct information in the UI.
  */
 
 add_task(function* () {
   let { L10N } = require("devtools/client/netmonitor/src/utils/l10n");
 
   // Set a higher panel height in order to get full CodeMirror content
-  Services.prefs.setIntPref("devtools.toolbox.footer.height", 400);
+  Services.prefs.setIntPref("devtools.toolbox.footer.height", 600);
 
   let { tab, monitor } = yield initNetMonitor(POST_DATA_URL);
   info("Starting test... ");
 
   let { document, store, windowRequire } = monitor.panelWin;
   let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
   let {
     getDisplayedRequests,
@@ -134,17 +134,23 @@ add_task(function* () {
       is(values[3].textContent, "123", "The first post param value was incorrect.");
       is(labels[4].textContent, "foo", "The second post param name was incorrect.");
       is(values[4].textContent, "bar", "The second post param value was incorrect.");
     } else {
       checkVisibility("params editor");
 
       is(labels.length, 3, "There should be 3 param values displayed in this tabpanel.");
 
-      let text = document.querySelector(".CodeMirror-code").textContent;
+      // Collect code lines and combine into one text for checking
+      let text = "";
+      let lines = [...document.querySelectorAll(".CodeMirror-line")];
+
+      lines.forEach((line) => {
+        text += line.textContent + "\n";
+      });
 
       ok(text.includes("Content-Disposition: form-data; name=\"text\""),
         "The text shown in the source editor is incorrect (1.1).");
       ok(text.includes("Content-Disposition: form-data; name=\"email\""),
         "The text shown in the source editor is incorrect (2.1).");
       ok(text.includes("Content-Disposition: form-data; name=\"range\""),
         "The text shown in the source editor is incorrect (3.1).");
       ok(text.includes("Content-Disposition: form-data; name=\"Custom field\""),
--- a/devtools/client/performance/modules/widgets/graphs.js
+++ b/devtools/client/performance/modules/widgets/graphs.js
@@ -3,17 +3,17 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 /**
  * This file contains the base line graph that all Performance line graphs use.
  */
 
 const { Task } = require("devtools/shared/task");
-const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
+const { extend } = require("devtools/shared/extend");
 const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
 const MountainGraphWidget = require("devtools/client/shared/widgets/MountainGraphWidget");
 const { CanvasGraphUtils } = require("devtools/client/shared/widgets/Graphs");
 
 const defer = require("devtools/shared/defer");
 const EventEmitter = require("devtools/shared/old-event-emitter");
 
 const { colorUtils } = require("devtools/shared/css/color");
@@ -54,17 +54,17 @@ const OPTIMIZATIONS_GRAPH_RESOLUTION = 1
  * @param string metric
  *        The unit of measurement for this graph.
  */
 function PerformanceGraph(parent, metric) {
   LineGraphWidget.call(this, parent, { metric });
   this.setTheme();
 }
 
-PerformanceGraph.prototype = Heritage.extend(LineGraphWidget.prototype, {
+PerformanceGraph.prototype = extend(LineGraphWidget.prototype, {
   strokeWidth: STROKE_WIDTH,
   dampenValuesFactor: DAMPEN_VALUES,
   fixedHeight: HEIGHT,
   clipheadLineColor: CLIPHEAD_LINE_COLOR,
   selectionLineColor: SELECTION_LINE_COLOR,
   withTooltipArrows: false,
   withFixedTooltipPositions: true,
 
@@ -103,17 +103,17 @@ PerformanceGraph.prototype = Heritage.ex
  *
  * @param nsIDOMNode parent
  *        The parent node holding the overview.
  */
 function FramerateGraph(parent) {
   PerformanceGraph.call(this, parent, ProfilerGlobal.L10N.getStr("graphs.fps"));
 }
 
-FramerateGraph.prototype = Heritage.extend(PerformanceGraph.prototype, {
+FramerateGraph.prototype = extend(PerformanceGraph.prototype, {
   mainColor: FRAMERATE_GRAPH_COLOR_NAME,
   setPerformanceData: function ({ duration, ticks }, resolution) {
     this.dataDuration = duration;
     return this.setDataFromTimestamps(ticks, resolution, duration);
   }
 });
 
 /**
@@ -121,29 +121,29 @@ FramerateGraph.prototype = Heritage.exte
  *
  * @param nsIDOMNode parent
  *        The parent node holding the overview.
  */
 function MemoryGraph(parent) {
   PerformanceGraph.call(this, parent, ProfilerGlobal.L10N.getStr("graphs.memory"));
 }
 
-MemoryGraph.prototype = Heritage.extend(PerformanceGraph.prototype, {
+MemoryGraph.prototype = extend(PerformanceGraph.prototype, {
   mainColor: MEMORY_GRAPH_COLOR_NAME,
   setPerformanceData: function ({ duration, memory }) {
     this.dataDuration = duration;
     return this.setData(memory);
   }
 });
 
 function TimelineGraph(parent, filter) {
   MarkersOverview.call(this, parent, filter);
 }
 
-TimelineGraph.prototype = Heritage.extend(MarkersOverview.prototype, {
+TimelineGraph.prototype = extend(MarkersOverview.prototype, {
   headerHeight: MARKERS_GRAPH_HEADER_HEIGHT,
   rowHeight: MARKERS_GRAPH_ROW_HEIGHT,
   groupPadding: MARKERS_GROUP_VERTICAL_PADDING,
   setPerformanceData: MarkersOverview.prototype.setData
 });
 
 /**
  * Definitions file for GraphsController, indicating the constructor,
@@ -434,17 +434,17 @@ GraphsController.prototype = {
  * @param string metric
  *        The unit of measurement for this graph.
  */
 function OptimizationsGraph(parent) {
   MountainGraphWidget.call(this, parent);
   this.setTheme();
 }
 
-OptimizationsGraph.prototype = Heritage.extend(MountainGraphWidget.prototype, {
+OptimizationsGraph.prototype = extend(MountainGraphWidget.prototype, {
 
   render: Task.async(function* (threadNode, frameNode) {
     // Regardless if we draw or clear the graph, wait
     // until it's ready.
     yield this.ready();
 
     if (!threadNode || !frameNode) {
       this.setData([]);
--- a/devtools/client/performance/modules/widgets/markers-overview.js
+++ b/devtools/client/performance/modules/widgets/markers-overview.js
@@ -4,17 +4,17 @@
 "use strict";
 
 /**
  * This file contains the "markers overview" graph, which is a minimap of all
  * the timeline data. Regions inside it may be selected, determining which
  * markers are visible in the "waterfall".
  */
 
-const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
+const { extend } = require("devtools/shared/extend");
 const { AbstractCanvasGraph } = require("devtools/client/shared/widgets/Graphs");
 
 const { colorUtils } = require("devtools/shared/css/color");
 const { getColor } = require("devtools/client/shared/theme");
 const ProfilerGlobal = require("devtools/client/performance/modules/global");
 const { MarkerBlueprintUtils } = require("devtools/client/performance/modules/marker-blueprint-utils");
 const { TickUtils } = require("devtools/client/performance/modules/waterfall-ticks");
 const { TIMELINE_BLUEPRINT } = require("devtools/client/performance/modules/markers");
@@ -43,17 +43,17 @@ const OVERVIEW_GROUP_VERTICAL_PADDING = 
  *        List of names of marker types that should not be shown.
  */
 function MarkersOverview(parent, filter = [], ...args) {
   AbstractCanvasGraph.apply(this, [parent, "markers-overview", ...args]);
   this.setTheme();
   this.setFilter(filter);
 }
 
-MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
+MarkersOverview.prototype = extend(AbstractCanvasGraph.prototype, {
   clipheadLineColor: OVERVIEW_CLIPHEAD_LINE_COLOR,
   selectionLineColor: OVERVIEW_SELECTION_LINE_COLOR,
   headerHeight: OVERVIEW_HEADER_HEIGHT,
   rowHeight: OVERVIEW_ROW_HEIGHT,
   groupPadding: OVERVIEW_GROUP_VERTICAL_PADDING,
 
   /**
    * Compute the height of the overview.
--- a/devtools/client/performance/modules/widgets/tree-view.js
+++ b/devtools/client/performance/modules/widgets/tree-view.js
@@ -4,17 +4,17 @@
 "use strict";
 
 /**
  * This file contains the tree view, displaying all the samples and frames
  * received from the proviler in a tree-like structure.
  */
 
 const { L10N } = require("devtools/client/performance/modules/global");
-const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
+const { extend } = require("devtools/shared/extend");
 const { AbstractTreeItem } = require("resource://devtools/client/shared/widgets/AbstractTreeItem.jsm");
 
 const URL_LABEL_TOOLTIP = L10N.getStr("table.url.tooltiptext");
 const VIEW_OPTIMIZATIONS_TOOLTIP = L10N.getStr("table.view-optimizations.tooltiptext2");
 
 const CALL_TREE_INDENTATION = 16; // px
 
 // Used for rendering values in cells
@@ -169,17 +169,17 @@ function CallView({
   this.frame = frame;
   this.hidden = hidden;
   this.inverted = inverted;
   this.showOptimizationHint = showOptimizationHint;
 
   this._onUrlClick = this._onUrlClick.bind(this);
 }
 
-CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
+CallView.prototype = extend(AbstractTreeItem.prototype, {
   /**
    * Creates the view for this tree node.
    * @param nsIDOMNode document
    * @param nsIDOMNode arrowNode
    * @return nsIDOMNode
    */
   _displaySelf: function (document, arrowNode) {
     let frameInfo = this.getDisplayedData();
--- a/devtools/client/performance/performance-controller.js
+++ b/devtools/client/performance/performance-controller.js
@@ -9,20 +9,21 @@
 var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
 var BrowserLoaderModule = {};
 Cu.import("resource://devtools/client/shared/browser-loader.js", BrowserLoaderModule);
 var { loader, require } = BrowserLoaderModule.BrowserLoader({
   baseURI: "resource://devtools/client/performance/",
   window
 });
 var { Task } = require("devtools/shared/task");
-/* exported Heritage, ViewHelpers, WidgetMethods, setNamedTimeout, clearNamedTimeout */
-var { Heritage, ViewHelpers, WidgetMethods, setNamedTimeout, clearNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
+/* exported ViewHelpers, WidgetMethods, setNamedTimeout, clearNamedTimeout */
+var { ViewHelpers, WidgetMethods, setNamedTimeout, clearNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
 var { PrefObserver } = require("devtools/client/shared/prefs");
-
+/* exported extend */
+const { extend } = require("devtools/shared/extend");
 // Use privileged promise in panel documents to prevent having them to freeze
 // during toolbox destruction. See bug 1402779.
 var Promise = require("Promise");
 
 // Events emitted by various objects in the panel.
 var EVENTS = require("devtools/client/performance/events");
 Object.defineProperty(this, "EVENTS", {
   value: EVENTS,
--- a/devtools/client/performance/test/helpers/synth-utils.js
+++ b/devtools/client/performance/test/helpers/synth-utils.js
@@ -50,24 +50,24 @@ exports.synthesizeProfile = () => {
 };
 
 /**
  * Generates a simple implementation for a tree class.
  */
 exports.synthesizeCustomTreeClass = () => {
   const { Cu } = require("chrome");
   const { AbstractTreeItem } = Cu.import("resource://devtools/client/shared/widgets/AbstractTreeItem.jsm", {});
-  const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
+  const { extend } = require("devtools/shared/extend");
 
   function MyCustomTreeItem(dataSrc, properties) {
     AbstractTreeItem.call(this, properties);
     this.itemDataSrc = dataSrc;
   }
 
-  MyCustomTreeItem.prototype = Heritage.extend(AbstractTreeItem.prototype, {
+  MyCustomTreeItem.prototype = extend(AbstractTreeItem.prototype, {
     _displaySelf: function (document, arrowNode) {
       let node = document.createElement("hbox");
       node.style.marginInlineStart = (this.level * 10) + "px";
       node.appendChild(arrowNode);
       node.appendChild(document.createTextNode(this.itemDataSrc.label));
       return node;
     },
 
--- a/devtools/client/performance/views/details-js-call-tree.js
+++ b/devtools/client/performance/views/details-js-call-tree.js
@@ -4,17 +4,17 @@
 /* import-globals-from ../performance-controller.js */
 /* import-globals-from ../performance-view.js */
 /* globals DetailsSubview */
 "use strict";
 
 /**
  * CallTree view containing profiler call tree, controlled by DetailsView.
  */
-var JsCallTreeView = Heritage.extend(DetailsSubview, {
+var JsCallTreeView = extend(DetailsSubview, {
 
   rerenderPrefs: [
     "invert-call-tree",
     "show-platform-data",
     "flatten-tree-recursion",
     "show-jit-optimizations",
   ],
 
--- a/devtools/client/performance/views/details-js-flamegraph.js
+++ b/devtools/client/performance/views/details-js-flamegraph.js
@@ -5,17 +5,17 @@
 /* import-globals-from ../performance-view.js */
 /* globals DetailsSubview */
 "use strict";
 
 /**
  * FlameGraph view containing a pyramid-like visualization of a profile,
  * controlled by DetailsView.
  */
-var JsFlameGraphView = Heritage.extend(DetailsSubview, {
+var JsFlameGraphView = extend(DetailsSubview, {
 
   shouldUpdateWhileMouseIsActive: true,
 
   rerenderPrefs: [
     "invert-flame-graph",
     "flatten-tree-recursion",
     "show-platform-data",
     "show-idle-blocks"
--- a/devtools/client/performance/views/details-memory-call-tree.js
+++ b/devtools/client/performance/views/details-memory-call-tree.js
@@ -4,17 +4,17 @@
 /* import-globals-from ../performance-controller.js */
 /* import-globals-from ../performance-view.js */
 /* globals DetailsSubview */
 "use strict";
 
 /**
  * CallTree view containing memory allocation sites, controlled by DetailsView.
  */
-var MemoryCallTreeView = Heritage.extend(DetailsSubview, {
+var MemoryCallTreeView = extend(DetailsSubview, {
 
   rerenderPrefs: [
     "invert-call-tree"
   ],
 
   // Units are in milliseconds.
   rangeChangeDebounceTime: 100,
 
--- a/devtools/client/performance/views/details-memory-flamegraph.js
+++ b/devtools/client/performance/views/details-memory-flamegraph.js
@@ -5,17 +5,17 @@
 /* import-globals-from ../performance-view.js */
 /* globals DetailsSubview */
 "use strict";
 
 /**
  * FlameGraph view containing a pyramid-like visualization of memory allocation
  * sites, controlled by DetailsView.
  */
-var MemoryFlameGraphView = Heritage.extend(DetailsSubview, {
+var MemoryFlameGraphView = extend(DetailsSubview, {
 
   shouldUpdateWhileMouseIsActive: true,
 
   rerenderPrefs: [
     "invert-flame-graph",
     "flatten-tree-recursion",
     "show-idle-blocks"
   ],
--- a/devtools/client/performance/views/details-waterfall.js
+++ b/devtools/client/performance/views/details-waterfall.js
@@ -10,17 +10,17 @@ const MARKER_DETAILS_WIDTH = 200;
 // Units are in milliseconds.
 const WATERFALL_RESIZE_EVENTS_DRAIN = 100;
 
 const { TickUtils } = require("devtools/client/performance/modules/waterfall-ticks");
 
 /**
  * Waterfall view containing the timeline markers, controlled by DetailsView.
  */
-var WaterfallView = Heritage.extend(DetailsSubview, {
+var WaterfallView = extend(DetailsSubview, {
 
   // Smallest unit of time between two markers. Larger by 10x^3 than Number.EPSILON.
   MARKER_EPSILON: 0.000000000001,
   // px
   WATERFALL_MARKER_SIDEBAR_WIDTH: 175,
   // px
   WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS: 20,
 
--- a/devtools/client/scratchpad/scratchpad.js
+++ b/devtools/client/scratchpad/scratchpad.js
@@ -50,17 +50,17 @@ const TargetFactory = require("devtools/
 const EventEmitter = require("devtools/shared/old-event-emitter");
 const {DevToolsWorker} = require("devtools/shared/worker/worker");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 const flags = require("devtools/shared/flags");
 const promise = require("promise");
 const defer = require("devtools/shared/defer");
 const Services = require("Services");
 const {gDevTools} = require("devtools/client/framework/devtools");
-const {Heritage} = require("devtools/client/shared/widgets/view-helpers");
+const { extend } = require("devtools/shared/extend");
 
 const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
 const {NetUtil} = require("resource://gre/modules/NetUtil.jsm");
 const {ScratchpadManager} = require("resource://devtools/client/scratchpad/scratchpad-manager.jsm");
 const {addDebuggerToGlobal} = require("resource://gre/modules/jsdebugger.jsm");
 const {OS} = require("resource://gre/modules/osfile.jsm");
 const {Reflect} = require("resource://gre/modules/reflect.jsm");
 
@@ -2172,17 +2172,17 @@ ScratchpadTab.prototype = {
 /**
  * Represents the DebuggerClient connection to a specific window as used by the
  * Scratchpad.
  */
 function ScratchpadWindow() {}
 
 ScratchpadWindow.consoleFor = ScratchpadTab.consoleFor;
 
-ScratchpadWindow.prototype = Heritage.extend(ScratchpadTab.prototype, {
+ScratchpadWindow.prototype = extend(ScratchpadTab.prototype, {
   /**
    * Attach to this window.
    *
    * @return Promise
    *         The promise for the target for this window.
    */
   _attach: function SW__attach()
   {
@@ -2202,17 +2202,17 @@ ScratchpadWindow.prototype = Heritage.ex
 
 function ScratchpadTarget(aTarget)
 {
   this._target = aTarget;
 }
 
 ScratchpadTarget.consoleFor = ScratchpadTab.consoleFor;
 
-ScratchpadTarget.prototype = Heritage.extend(ScratchpadTab.prototype, {
+ScratchpadTarget.prototype = extend(ScratchpadTab.prototype, {
   _attach: function ST__attach()
   {
     if (this._target.isRemote) {
       return promise.resolve(this._target);
     }
     return this._target.makeRemote().then(() => this._target);
   }
 });
--- a/devtools/client/shadereditor/shadereditor.js
+++ b/devtools/client/shadereditor/shadereditor.js
@@ -10,17 +10,18 @@ const {XPCOMUtils} = require("resource:/
 const {SideMenuWidget} = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
 const promise = require("promise");
 const defer = require("devtools/shared/defer");
 const Services = require("Services");
 const EventEmitter = require("devtools/shared/old-event-emitter");
 const Tooltip = require("devtools/client/shared/widgets/tooltip/Tooltip");
 const Editor = require("devtools/client/sourceeditor/editor");
 const {LocalizationHelper} = require("devtools/shared/l10n");
-const {Heritage, WidgetMethods, setNamedTimeout} =
+const {extend} = require("devtools/shared/extend");
+const {WidgetMethods, setNamedTimeout} =
   require("devtools/client/shared/widgets/view-helpers");
 const {Task} = require("devtools/shared/task");
 
 // Use privileged promise in panel documents to prevent having them to freeze
 // during toolbox destruction. See bug 1402779.
 const Promise = require("Promise");
 
 // The panel's window global is an EventEmitter firing the following events:
@@ -193,17 +194,17 @@ var EventsHandler = {
     $("#content").hidden = false;
     ShadersListView.addProgram(programActor);
   }
 };
 
 /**
  * Functions handling the sources UI.
  */
-var ShadersListView = Heritage.extend(WidgetMethods, {
+var ShadersListView = extend(WidgetMethods, {
   /**
    * Initialization function, called when the tool is started.
    */
   initialize: function () {
     this.widget = new SideMenuWidget(this._pane = $("#shaders-pane"), {
       showArrows: true,
       showItemCheckboxes: true
     });
--- a/devtools/client/shared/widgets/AbstractTreeItem.jsm
+++ b/devtools/client/shared/widgets/AbstractTreeItem.jsm
@@ -36,17 +36,17 @@ this.EXPORTED_SYMBOLS = ["AbstractTreeIt
  *
  * For example, you can extend this abstract class like this:
  *
  * function MyCustomTreeItem(dataSrc, properties) {
  *   AbstractTreeItem.call(this, properties);
  *   this.itemDataSrc = dataSrc;
  * }
  *
- * MyCustomTreeItem.prototype = Heritage.extend(AbstractTreeItem.prototype, {
+ * MyCustomTreeItem.prototype = extend(AbstractTreeItem.prototype, {
  *   _displaySelf: function(document, arrowNode) {
  *     let node = document.createElement("hbox");
  *     ...
  *     // Append the provided arrow node wherever you want.
  *     node.appendChild(arrowNode);
  *     ...
  *     // Use `this.itemDataSrc` to customize the tree item and
  *     // `this.level` to calculate the indentation.
--- a/devtools/client/shared/widgets/BarGraphWidget.js
+++ b/devtools/client/shared/widgets/BarGraphWidget.js
@@ -1,11 +1,12 @@
 "use strict";
 
-const { Heritage, setNamedTimeout, clearNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
+const { extend } = require("devtools/shared/extend");
+const { setNamedTimeout, clearNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
 const { AbstractCanvasGraph, CanvasGraphUtils } = require("devtools/client/shared/widgets/Graphs");
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
 // Bar graph constants.
 
 const GRAPH_DAMPEN_VALUES_FACTOR = 0.75;
 
@@ -72,17 +73,17 @@ this.BarGraphWidget = function (parent, 
     this._onLegendMouseOver = this._onLegendMouseOver.bind(this);
     this._onLegendMouseOut = this._onLegendMouseOut.bind(this);
     this._onLegendMouseDown = this._onLegendMouseDown.bind(this);
     this._onLegendMouseUp = this._onLegendMouseUp.bind(this);
     this._createLegend();
   });
 };
 
-BarGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
+BarGraphWidget.prototype = extend(AbstractCanvasGraph.prototype, {
   clipheadLineColor: GRAPH_CLIPHEAD_LINE_COLOR,
   selectionLineColor: GRAPH_SELECTION_LINE_COLOR,
   selectionBackgroundColor: GRAPH_SELECTION_BACKGROUND_COLOR,
   selectionStripesColor: GRAPH_SELECTION_STRIPES_COLOR,
   regionBackgroundColor: GRAPH_REGION_BACKGROUND_COLOR,
   regionStripesColor: GRAPH_REGION_STRIPES_COLOR,
 
   /**
--- a/devtools/client/shared/widgets/LineGraphWidget.js
+++ b/devtools/client/shared/widgets/LineGraphWidget.js
@@ -1,12 +1,12 @@
 "use strict";
 
 const { Task } = require("devtools/shared/task");
-const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
+const { extend } = require("devtools/shared/extend");
 const { AbstractCanvasGraph, CanvasGraphUtils } = require("devtools/client/shared/widgets/Graphs");
 const { LocalizationHelper } = require("devtools/shared/l10n");
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const L10N = new LocalizationHelper("devtools/client/locales/graphs.properties");
 
 // Line graph constants.
 
@@ -84,17 +84,17 @@ this.LineGraphWidget = function (parent,
     );
     this._avgGutterLine = this._createGutterLine("average");
     this._avgTooltip = this._createTooltip(
       "average", "end", L10N.getStr("graphs.label.average"), metric
     );
   });
 };
 
-LineGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
+LineGraphWidget.prototype = extend(AbstractCanvasGraph.prototype, {
   backgroundColor: GRAPH_BACKGROUND_COLOR,
   backgroundGradientStart: GRAPH_BACKGROUND_GRADIENT_START,
   backgroundGradientEnd: GRAPH_BACKGROUND_GRADIENT_END,
   strokeColor: GRAPH_STROKE_COLOR,
   strokeWidth: GRAPH_STROKE_WIDTH,
   maximumLineColor: GRAPH_MAXIMUM_LINE_COLOR,
   averageLineColor: GRAPH_AVERAGE_LINE_COLOR,
   minimumLineColor: GRAPH_MINIMUM_LINE_COLOR,
--- a/devtools/client/shared/widgets/MountainGraphWidget.js
+++ b/devtools/client/shared/widgets/MountainGraphWidget.js
@@ -1,11 +1,11 @@
 "use strict";
 
-const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
+const { extend } = require("devtools/shared/extend");
 const { AbstractCanvasGraph } = require("devtools/client/shared/widgets/Graphs");
 
 // Bar graph constants.
 
 const GRAPH_DAMPEN_VALUES_FACTOR = 0.9;
 
 const GRAPH_BACKGROUND_COLOR = "#ddd";
 const GRAPH_STROKE_WIDTH = 1; // px
@@ -52,17 +52,17 @@ const GRAPH_REGION_STRIPES_COLOR = "rgba
  *
  * @param nsIDOMNode parent
  *        The parent node holding the graph.
  */
 this.MountainGraphWidget = function (parent, ...args) {
   AbstractCanvasGraph.apply(this, [parent, "mountain-graph", ...args]);
 };
 
-MountainGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
+MountainGraphWidget.prototype = extend(AbstractCanvasGraph.prototype, {
   backgroundColor: GRAPH_BACKGROUND_COLOR,
   strokeColor: GRAPH_STROKE_COLOR,
   strokeWidth: GRAPH_STROKE_WIDTH,
   clipheadLineColor: GRAPH_CLIPHEAD_LINE_COLOR,
   selectionLineColor: GRAPH_SELECTION_LINE_COLOR,
   selectionBackgroundColor: GRAPH_SELECTION_BACKGROUND_COLOR,
   selectionStripesColor: GRAPH_SELECTION_STRIPES_COLOR,
   regionBackgroundColor: GRAPH_REGION_BACKGROUND_COLOR,
--- a/devtools/client/shared/widgets/VariablesView.jsm
+++ b/devtools/client/shared/widgets/VariablesView.jsm
@@ -19,17 +19,18 @@ const ITEM_FLASH_DURATION = 300; // ms
 const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
 const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
 const EventEmitter = require("devtools/shared/old-event-emitter");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 const Services = require("Services");
 const { getSourceNames } = require("devtools/client/shared/source-utils");
 const promise = require("promise");
 const defer = require("devtools/shared/defer");
-const { Heritage, ViewHelpers, setNamedTimeout } =
+const { extend } = require("devtools/shared/extend");
+const { ViewHelpers, setNamedTimeout } =
   require("devtools/client/shared/widgets/view-helpers");
 const { Task } = require("devtools/shared/task");
 const nodeConstants = require("devtools/shared/dom-node-constants");
 const {KeyCodes} = require("devtools/client/shared/keycodes");
 const {PluralForm} = require("devtools/shared/plural-form");
 const {LocalizationHelper, ELLIPSIS} = require("devtools/shared/l10n");
 const L10N = new LocalizationHelper(DBG_STRINGS_URI);
 
@@ -2174,17 +2175,17 @@ function Variable(aScope, aName, aDescri
     delete aDescriptor.get;
     delete aDescriptor.set;
   }
 
   Scope.call(this, aScope, aName, this._initialDescriptor = aDescriptor);
   this.setGrip(aDescriptor.value);
 }
 
-Variable.prototype = Heritage.extend(Scope.prototype, {
+Variable.prototype = extend(Scope.prototype, {
   /**
    * Whether this Variable should be prefetched when it is remoted.
    */
   get shouldPrefetch() {
     return this.name == "window" || this.name == "this";
   },
 
   /**
@@ -3067,17 +3068,17 @@ Variable.prototype = Heritage.extend(Sco
  *        The property's descriptor.
  * @param object aOptions
  *        Options of the form accepted by Scope.addItem
  */
 function Property(aVar, aName, aDescriptor, aOptions) {
   Variable.call(this, aVar, aName, aDescriptor, aOptions);
 }
 
-Property.prototype = Heritage.extend(Variable.prototype, {
+Property.prototype = extend(Variable.prototype, {
   /**
    * The class name applied to this property's target element.
    */
   targetClassName: "variables-view-property variable-or-property",
 
   /**
    * @see Variable.symbolicName
    * @return string
@@ -3155,17 +3156,17 @@ VariablesView.prototype.commitHierarchy 
   }
   if (this.oncommit) {
     this.oncommit(this);
   }
 };
 
 // Some variables are likely to contain a very large number of properties.
 // It would be a bad idea to re-expand them or perform expensive operations.
-VariablesView.prototype.commitHierarchyIgnoredItems = Heritage.extend(null, {
+VariablesView.prototype.commitHierarchyIgnoredItems = extend(null, {
   "window": true,
   "this": true
 });
 
 /**
  * Checks if the an item was previously expanded, if it existed in a
  * previous hierarchy.
  *
@@ -4111,17 +4112,17 @@ Editable.prototype = {
  * An Editable specific to editing the name of a Variable or Property.
  */
 function EditableName(aVariable, aOptions) {
   Editable.call(this, aVariable, aOptions);
 }
 
 EditableName.create = Editable.create;
 
-EditableName.prototype = Heritage.extend(Editable.prototype, {
+EditableName.prototype = extend(Editable.prototype, {
   className: "element-name-input",
 
   get label() {
     return this._variable._name;
   },
 
   get shouldActivate() {
     return !!this._variable.ownerView.switch;
@@ -4133,17 +4134,17 @@ EditableName.prototype = Heritage.extend
  * An Editable specific to editing the value of a Variable or Property.
  */
 function EditableValue(aVariable, aOptions) {
   Editable.call(this, aVariable, aOptions);
 }
 
 EditableValue.create = Editable.create;
 
-EditableValue.prototype = Heritage.extend(Editable.prototype, {
+EditableValue.prototype = extend(Editable.prototype, {
   className: "element-value-input",
 
   get label() {
     return this._variable._valueLabel;
   },
 
   get shouldActivate() {
     return !!this._variable.ownerView.eval;
@@ -4155,17 +4156,17 @@ EditableValue.prototype = Heritage.exten
  * An Editable specific to editing the key and value of a new property.
  */
 function EditableNameAndValue(aVariable, aOptions) {
   EditableName.call(this, aVariable, aOptions);
 }
 
 EditableNameAndValue.create = Editable.create;
 
-EditableNameAndValue.prototype = Heritage.extend(EditableName.prototype, {
+EditableNameAndValue.prototype = extend(EditableName.prototype, {
   _reset: function (e) {
     // Hide the Variable or Property if the user presses escape.
     this._variable.remove();
     this.deactivate();
   },
 
   _next: function (e) {
     // Override _next so as to set both key and value at the same time.
--- a/devtools/client/shared/widgets/view-helpers.js
+++ b/devtools/client/shared/widgets/view-helpers.js
@@ -9,39 +9,16 @@ const {KeyCodes} = require("devtools/cli
 
 const PANE_APPEARANCE_DELAY = 50;
 const PAGE_SIZE_ITEM_COUNT_RATIO = 5;
 const WIDGET_FOCUSABLE_NODES = new Set(["vbox", "hbox"]);
 
 var namedTimeoutsStore = new Map();
 
 /**
- * Inheritance helpers from the addon SDK's core/heritage.
- * Remove these when all devtools are loadered.
- */
-exports.Heritage = {
-  /**
-   * @see extend in sdk/core/heritage.
-   */
-  extend: function (prototype, properties = {}) {
-    return Object.create(prototype, this.getOwnPropertyDescriptors(properties));
-  },
-
-  /**
-   * @see getOwnPropertyDescriptors in sdk/core/heritage.
-   */
-  getOwnPropertyDescriptors: function (object) {
-    return Object.getOwnPropertyNames(object).reduce((descriptor, name) => {
-      descriptor[name] = Object.getOwnPropertyDescriptor(object, name);
-      return descriptor;
-    }, {});
-  }
-};
-
-/**
  * Helper for draining a rapid succession of events and invoking a callback
  * once everything settles down.
  *
  * @param string id
  *        A string identifier for the named timeout.
  * @param number wait
  *        The amount of milliseconds to wait after no more events are fired.
  * @param function callback
@@ -485,17 +462,17 @@ Item.prototype = {
  * Some generic Widget methods handling Item instances.
  * Iterable via "for (let childItem of wrappedView) { }".
  *
  * Usage:
  *   function MyView() {
  *     this.widget = new MyWidget(document.querySelector(".my-node"));
  *   }
  *
- *   MyView.prototype = Heritage.extend(WidgetMethods, {
+ *   MyView.prototype = extend(WidgetMethods, {
  *     myMethod: function() {},
  *     ...
  *   });
  *
  * See https://gist.github.com/victorporof/5749386 for more details.
  * The devtools/shared/widgets/SimpleListWidget.jsm is an implementation
  * example.
  *
--- a/devtools/client/styleeditor/test/browser_styleeditor_fetch-from-cache.js
+++ b/devtools/client/styleeditor/test/browser_styleeditor_fetch-from-cache.js
@@ -1,11 +1,11 @@
-/* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
+
 "use strict";
 
 // A test to ensure Style Editor doesn't bybass cache when loading style sheet
 // contents (bug 978688).
 
 const TEST_URL = TEST_BASE_HTTP + "doc_uncached.html";
 
 add_task(function* () {
@@ -37,13 +37,11 @@ add_task(function* () {
   info("Checking Netmonitor contents.");
   let items = [];
   for (let item of getSortedRequests(store.getState())) {
     if (item.url.endsWith("doc_uncached.css")) {
       items.push(item);
     }
   }
 
-  is(items.length, 2,
-     "Got two requests for doc_uncached.css after Style Editor was loaded.");
-  ok(items[1].fromCache,
-     "Second request was loaded from browser cache");
+  is(items.length, 1,
+     "Got one request for doc_uncached.css after Style Editor was loaded.");
 });
--- a/devtools/client/webconsole/new-console-output/test/components/console-api-call.test.js
+++ b/devtools/client/webconsole/new-console-output/test/components/console-api-call.test.js
@@ -120,20 +120,48 @@ describe("ConsoleAPICall component:", ()
       }));
 
       expect(wrapper.find(".timestamp").length).toBe(0);
     });
   });
 
   describe("console.count", () => {
     it("renders", () => {
-      const message = stubPreparedMessages.get("console.count('bar')");
-      const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+      const messages = [{
+        key: "console.count('bar')",
+        expectedBodyText: "bar: 1",
+      }, {
+        key: "console.count | default: 1",
+        expectedBodyText: "default: 1",
+      }, {
+        key: "console.count | default: 2",
+        expectedBodyText: "default: 2",
+      }, {
+        key: "console.count | test counter: 1",
+        expectedBodyText: "test counter: 1",
+      }, {
+        key: "console.count | test counter: 2",
+        expectedBodyText: "test counter: 2",
+      }, {
+        key: "console.count | default: 3",
+        expectedBodyText: "default: 3",
+      }, {
+        key: "console.count | default: 4",
+        expectedBodyText: "default: 4",
+      }, {
+        key: "console.count | test counter: 3",
+        expectedBodyText: "test counter: 3",
+      }];
 
-      expect(wrapper.find(".message-body").text()).toBe("bar: 1");
+      for (const {key, expectedBodyText} of messages) {
+        const message = stubPreparedMessages.get(key);
+        const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
+
+        expect(wrapper.find(".message-body").text()).toBe(expectedBodyText);
+      }
     });
   });
 
   describe("console.assert", () => {
     it("renders", () => {
       const message = stubPreparedMessages.get(
         "console.assert(false, {message: 'foobar'})");
       const wrapper = render(ConsoleApiCall({ message, serviceContainer }));
--- a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js
@@ -130,16 +130,38 @@ console.groupCollapsed(
 console.groupEnd();
 `});
 
 consoleApi.set("console.dir({C, M, Y, K})", {
   keys: ["console.dir({C, M, Y, K})"],
   code: "console.dir({cyan: 'C', magenta: 'M', yellow: 'Y', black: 'K'});"
 });
 
+consoleApi.set("console.count", {
+  keys: [
+    "console.count | default: 1",
+    "console.count | default: 2",
+    "console.count | test counter: 1",
+    "console.count | test counter: 2",
+    "console.count | default: 3",
+    "console.count | clear",
+    "console.count | default: 4",
+    "console.count | test counter: 3",
+  ],
+  code: `
+    console.count();
+    console.count();
+    console.count("test counter");
+    console.count("test counter");
+    console.count();
+    console.clear();
+    console.count();
+    console.count("test counter");
+`});
+
 // CSS messages
 const cssMessage = new Map();
 
 cssMessage.set("Unknown property", `
 p {
   such-unknown-property: wow;
 }
 `);
--- a/devtools/client/webconsole/new-console-output/test/fixtures/stubs/consoleApi.js
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/consoleApi.js
@@ -1292,16 +1292,210 @@ stubPreparedMessages.set("console.dir({C
   },
   "groupId": null,
   "exceptionDocURL": null,
   "userProvidedStyles": [],
   "notes": null,
   "indent": 0
 }));
 
+stubPreparedMessages.set("console.count | default: 1", new ConsoleMessage({
+  "id": "1",
+  "allowRepeating": true,
+  "source": "console-api",
+  "timeStamp": 1511365913333,
+  "type": "log",
+  "helperType": null,
+  "level": "log",
+  "messageText": "default: 1",
+  "parameters": null,
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":2,\"column\":5},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":\"default: 1\",\"parameters\":null,\"source\":\"console-api\",\"type\":\"log\",\"userProvidedStyles\":[]}",
+  "stacktrace": null,
+  "frame": {
+    "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
+    "line": 2,
+    "column": 5
+  },
+  "groupId": null,
+  "exceptionDocURL": null,
+  "userProvidedStyles": [],
+  "notes": null,
+  "indent": 0
+}));
+
+stubPreparedMessages.set("console.count | default: 2", new ConsoleMessage({
+  "id": "1",
+  "allowRepeating": true,
+  "source": "console-api",
+  "timeStamp": 1511365913334,
+  "type": "log",
+  "helperType": null,
+  "level": "log",
+  "messageText": "default: 2",
+  "parameters": null,
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":3,\"column\":5},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":\"default: 2\",\"parameters\":null,\"source\":\"console-api\",\"type\":\"log\",\"userProvidedStyles\":[]}",
+  "stacktrace": null,
+  "frame": {
+    "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
+    "line": 3,
+    "column": 5
+  },
+  "groupId": null,
+  "exceptionDocURL": null,
+  "userProvidedStyles": [],
+  "notes": null,
+  "indent": 0
+}));
+
+stubPreparedMessages.set("console.count | test counter: 1", new ConsoleMessage({
+  "id": "1",
+  "allowRepeating": true,
+  "source": "console-api",
+  "timeStamp": 1511365913334,
+  "type": "log",
+  "helperType": null,
+  "level": "log",
+  "messageText": "test counter: 1",
+  "parameters": null,
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":4,\"column\":5},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":\"test counter: 1\",\"parameters\":null,\"source\":\"console-api\",\"type\":\"log\",\"userProvidedStyles\":[]}",
+  "stacktrace": null,
+  "frame": {
+    "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
+    "line": 4,
+    "column": 5
+  },
+  "groupId": null,
+  "exceptionDocURL": null,
+  "userProvidedStyles": [],
+  "notes": null,
+  "indent": 0
+}));
+
+stubPreparedMessages.set("console.count | test counter: 2", new ConsoleMessage({
+  "id": "1",
+  "allowRepeating": true,
+  "source": "console-api",
+  "timeStamp": 1511365913334,
+  "type": "log",
+  "helperType": null,
+  "level": "log",
+  "messageText": "test counter: 2",
+  "parameters": null,
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":5,\"column\":5},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":\"test counter: 2\",\"parameters\":null,\"source\":\"console-api\",\"type\":\"log\",\"userProvidedStyles\":[]}",
+  "stacktrace": null,
+  "frame": {
+    "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
+    "line": 5,
+    "column": 5
+  },
+  "groupId": null,
+  "exceptionDocURL": null,
+  "userProvidedStyles": [],
+  "notes": null,
+  "indent": 0
+}));
+
+stubPreparedMessages.set("console.count | default: 3", new ConsoleMessage({
+  "id": "1",
+  "allowRepeating": true,
+  "source": "console-api",
+  "timeStamp": 1511365913334,
+  "type": "log",
+  "helperType": null,
+  "level": "log",
+  "messageText": "default: 3",
+  "parameters": null,
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":6,\"column\":5},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":\"default: 3\",\"parameters\":null,\"source\":\"console-api\",\"type\":\"log\",\"userProvidedStyles\":[]}",
+  "stacktrace": null,
+  "frame": {
+    "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
+    "line": 6,
+    "column": 5
+  },
+  "groupId": null,
+  "exceptionDocURL": null,
+  "userProvidedStyles": [],
+  "notes": null,
+  "indent": 0
+}));
+
+stubPreparedMessages.set("console.count | clear", new ConsoleMessage({
+  "id": "1",
+  "allowRepeating": true,
+  "source": "console-api",
+  "timeStamp": 1511365913334,
+  "type": "clear",
+  "helperType": null,
+  "level": "log",
+  "messageText": null,
+  "parameters": [
+    "Console was cleared."
+  ],
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":7,\"column\":5},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":null,\"parameters\":[\"Console was cleared.\"],\"source\":\"console-api\",\"type\":\"clear\",\"userProvidedStyles\":[]}",
+  "stacktrace": null,
+  "frame": {
+    "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
+    "line": 7,
+    "column": 5
+  },
+  "groupId": null,
+  "exceptionDocURL": null,
+  "userProvidedStyles": [],
+  "notes": null,
+  "indent": 0
+}));
+
+stubPreparedMessages.set("console.count | default: 4", new ConsoleMessage({
+  "id": "1",
+  "allowRepeating": true,
+  "source": "console-api",
+  "timeStamp": 1511365913335,
+  "type": "log",
+  "helperType": null,
+  "level": "log",
+  "messageText": "default: 4",
+  "parameters": null,
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":8,\"column\":5},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":\"default: 4\",\"parameters\":null,\"source\":\"console-api\",\"type\":\"log\",\"userProvidedStyles\":[]}",
+  "stacktrace": null,
+  "frame": {
+    "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
+    "line": 8,
+    "column": 5
+  },
+  "groupId": null,
+  "exceptionDocURL": null,
+  "userProvidedStyles": [],
+  "notes": null,
+  "indent": 0
+}));
+
+stubPreparedMessages.set("console.count | test counter: 3", new ConsoleMessage({
+  "id": "1",
+  "allowRepeating": true,
+  "source": "console-api",
+  "timeStamp": 1511365913335,
+  "type": "log",
+  "helperType": null,
+  "level": "log",
+  "messageText": "test counter: 3",
+  "parameters": null,
+  "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html\",\"line\":9,\"column\":5},\"groupId\":null,\"indent\":0,\"level\":\"log\",\"messageText\":\"test counter: 3\",\"parameters\":null,\"source\":\"console-api\",\"type\":\"log\",\"userProvidedStyles\":[]}",
+  "stacktrace": null,
+  "frame": {
+    "source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
+    "line": 9,
+    "column": 5
+  },
+  "groupId": null,
+  "exceptionDocURL": null,
+  "userProvidedStyles": [],
+  "notes": null,
+  "indent": 0
+}));
+
 stubPackets.set("console.log('foobar', 'test')", {
   "from": "server1.conn0.child1/consoleActor2",
   "type": "consoleAPICall",
   "message": {
     "addonId": "",
     "arguments": [
       "foobar",
       "test"
@@ -2519,12 +2713,223 @@ stubPackets.set("console.dir({C, M, Y, K
     "timeStamp": 1502884924899,
     "timer": null,
     "workerType": "none",
     "styles": [],
     "category": "webdev"
   }
 });
 
+stubPackets.set("console.count | default: 1", {
+  "from": "server1.conn0.child1/consoleActor2",
+  "type": "consoleAPICall",
+  "message": {
+    "addonId": "",
+    "arguments": [
+      "default"
+    ],
+    "columnNumber": 5,
+    "counter": {
+      "count": 1,
+      "label": "default"
+    },
+    "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
+    "functionName": "triggerPacket",
+    "groupName": "",
+    "level": "count",
+    "lineNumber": 2,
+    "private": false,
+    "timeStamp": 1511365913333,
+    "timer": null,
+    "workerType": "none",
+    "styles": [],
+    "category": "webdev"
+  }
+});
+
+stubPackets.set("console.count | default: 2", {
+  "from": "server1.conn0.child1/consoleActor2",
+  "type": "consoleAPICall",
+  "message": {
+    "addonId": "",
+    "arguments": [
+      "default"
+    ],
+    "columnNumber": 5,
+    "counter": {
+      "count": 2,
+      "label": "default"
+    },
+    "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
+    "functionName": "triggerPacket",
+    "groupName": "",
+    "level": "count",
+    "lineNumber": 3,
+    "private": false,
+    "timeStamp": 1511365913334,
+    "timer": null,
+    "workerType": "none",
+    "styles": [],
+    "category": "webdev"
+  }
+});
+
+stubPackets.set("console.count | test counter: 1", {
+  "from": "server1.conn0.child1/consoleActor2",
+  "type": "consoleAPICall",
+  "message": {
+    "addonId": "",
+    "arguments": [
+      "test counter"
+    ],
+    "columnNumber": 5,
+    "counter": {
+      "count": 1,
+      "label": "test counter"
+    },
+    "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
+    "functionName": "triggerPacket",
+    "groupName": "",
+    "level": "count",
+    "lineNumber": 4,
+    "private": false,
+    "timeStamp": 1511365913334,
+    "timer": null,
+    "workerType": "none",
+    "styles": [],
+    "category": "webdev"
+  }
+});
+
+stubPackets.set("console.count | test counter: 2", {
+  "from": "server1.conn0.child1/consoleActor2",
+  "type": "consoleAPICall",
+  "message": {
+    "addonId": "",
+    "arguments": [
+      "test counter"
+    ],
+    "columnNumber": 5,
+    "counter": {
+      "count": 2,
+      "label": "test counter"
+    },
+    "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
+    "functionName": "triggerPacket",
+    "groupName": "",
+    "level": "count",
+    "lineNumber": 5,
+    "private": false,
+    "timeStamp": 1511365913334,
+    "timer": null,
+    "workerType": "none",
+    "styles": [],
+    "category": "webdev"
+  }
+});
+
+stubPackets.set("console.count | default: 3", {
+  "from": "server1.conn0.child1/consoleActor2",
+  "type": "consoleAPICall",
+  "message": {
+    "addonId": "",
+    "arguments": [
+      "default"
+    ],
+    "columnNumber": 5,
+    "counter": {
+      "count": 3,
+      "label": "default"
+    },
+    "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
+    "functionName": "triggerPacket",
+    "groupName": "",
+    "level": "count",
+    "lineNumber": 6,
+    "private": false,
+    "timeStamp": 1511365913334,
+    "timer": null,
+    "workerType": "none",
+    "styles": [],
+    "category": "webdev"
+  }
+});
+
+stubPackets.set("console.count | clear", {
+  "from": "server1.conn0.child1/consoleActor2",
+  "type": "consoleAPICall",
+  "message": {
+    "addonId": "",
+    "arguments": [],
+    "columnNumber": 5,
+    "counter": null,
+    "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
+    "functionName": "triggerPacket",
+    "groupName": "",
+    "level": "clear",
+    "lineNumber": 7,
+    "private": false,
+    "timeStamp": 1511365913334,
+    "timer": null,
+    "workerType": "none",
+    "styles": [],
+    "category": "webdev"
+  }
+});
+
+stubPackets.set("console.count | default: 4", {
+  "from": "server1.conn0.child1/consoleActor2",
+  "type": "consoleAPICall",
+  "message": {
+    "addonId": "",
+    "arguments": [
+      "default"
+    ],
+    "columnNumber": 5,
+    "counter": {
+      "count": 4,
+      "label": "default"
+    },
+    "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
+    "functionName": "triggerPacket",
+    "groupName": "",
+    "level": "count",
+    "lineNumber": 8,
+    "private": false,
+    "timeStamp": 1511365913335,
+    "timer": null,
+    "workerType": "none",
+    "styles": [],
+    "category": "webdev"
+  }
+});
+
+stubPackets.set("console.count | test counter: 3", {
+  "from": "server1.conn0.child1/consoleActor2",
+  "type": "consoleAPICall",
+  "message": {
+    "addonId": "",
+    "arguments": [
+      "test counter"
+    ],
+    "columnNumber": 5,
+    "counter": {
+      "count": 3,
+      "label": "test counter"
+    },
+    "filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-console-api.html",
+    "functionName": "triggerPacket",
+    "groupName": "",
+    "level": "count",
+    "lineNumber": 9,
+    "private": false,
+    "timeStamp": 1511365913335,
+    "timer": null,
+    "workerType": "none",
+    "styles": [],
+    "category": "webdev"
+  }
+});
+
 module.exports = {
   stubPreparedMessages,
   stubPackets,
 };
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
@@ -267,19 +267,17 @@ subsuite = clipboard
 skip-if = (e10s && debug) || (e10s && os == 'win') # Bug 1221499 enabled these on windows
 [browser_webconsole_cspro.js]
 [browser_webconsole_document_focus.js]
 [browser_webconsole_duplicate_errors.js]
 skip-if = true #	Bug 1403907
 [browser_webconsole_errors_after_page_reload.js]
 [browser_webconsole_eval_in_debugger_stackframe.js]
 [browser_webconsole_eval_in_debugger_stackframe2.js]
-skip-if = true # Bug 1408893
 [browser_webconsole_execution_scope.js]
-skip-if = true #	Bug 1405333
 [browser_webconsole_external_script_errors.js]
 [browser_webconsole_file_uri.js]
 skip-if = true #	Bug 1404382
 [browser_webconsole_filter_scroll.js]
 skip-if = true #	Bug 1404392
 [browser_webconsole_filters.js]
 [browser_webconsole_filters_persist.js]
 [browser_webconsole_highlighter_console_helper.js]
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_eval_in_debugger_stackframe2.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_eval_in_debugger_stackframe2.js
@@ -5,67 +5,61 @@
 
 // Test to make sure that web console commands can fire while paused at a
 // breakpoint that was triggered from a JS call.  Relies on asynchronous js
 // evaluation over the protocol - see Bug 1088861.
 
 "use strict";
 
 const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
-                 "test/test-eval-in-stackframe.html";
+                 "new-console-output/test/mochitest/test-eval-in-stackframe.html";
 
-// Force the old debugger UI since it's directly used (see Bug 1301705)
-Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", false);
-registerCleanupFunction(function* () {
-  Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
-});
+add_task(async function () {
+  // Force the old debugger UI since it's directly used (see Bug 1301705).
+  await pushPref("devtools.debugger.new-debugger-frontend", false);
 
-add_task(function* () {
-  yield loadTab(TEST_URI);
-
-  info("open the web console");
-  let hud = yield openConsole();
-  let {jsterm} = hud;
+  info("open the console");
+  const hud = await openNewTabAndConsole(TEST_URI);
+  const {jsterm} = hud;
 
   info("open the debugger");
-  let {panelWin} = yield openDebugger();
-  let {DebuggerController} = panelWin;
-  let {activeThread} = DebuggerController;
+  let {panel} = await openDebugger();
+  let {activeThread} = panel.panelWin.DebuggerController;
+
+  const onFirstCallFramesAdded = activeThread.addOneTimeListener("framesadded");
+  // firstCall calls secondCall, which has a debugger statement, so we'll be paused.
+  const onFirstCallMessageReceived = waitForMessage(hud, "undefined");
 
-  let firstCall = defer();
-  let frameAdded = defer();
-  executeSoon(() => {
-    info("Executing firstCall");
-    activeThread.addOneTimeListener("framesadded", () => {
-      executeSoon(frameAdded.resolve);
-    });
-    jsterm.execute("firstCall()").then(firstCall.resolve);
+  const unresolvedSymbol = Symbol();
+  let firstCallEvaluationResult = unresolvedSymbol;
+  onFirstCallMessageReceived.then(message => {
+    firstCallEvaluationResult = message;
   });
+  jsterm.execute("firstCall()");
 
   info("Waiting for a frame to be added");
-  yield frameAdded.promise;
+  await onFirstCallFramesAdded;
+
+  info("frames added, select the console again");
+  await openConsole();
 
   info("Executing basic command while paused");
-  yield executeAndConfirm(jsterm, "1 + 2", "3");
+  let onMessageReceived = waitForMessage(hud, "3");
+  jsterm.execute("1 + 2");
+  let message = await onMessageReceived;
+  ok(message, "`1 + 2` was evaluated whith debugger paused");
 
   info("Executing command using scoped variables while paused");
-  yield executeAndConfirm(jsterm, "foo + foo2",
-                          '"globalFooBug783499foo2SecondCall"');
+  onMessageReceived = waitForMessage(hud, `"globalFooBug783499foo2SecondCall"`);
+  jsterm.execute("foo + foo2");
+  message = await onMessageReceived;
+  ok(message, "`foo + foo2` was evaluated as expected with debugger paused");
+
+  info("Checking the first command, which is the last to resolve since it paused");
+  ok(firstCallEvaluationResult === unresolvedSymbol, "firstCall was not evaluated yet");
 
   info("Resuming the thread");
   activeThread.resume();
 
-  info("Checking the first command, which is the last to resolve since it " +
-       "paused");
-  let node = yield firstCall.promise;
-  is(node.querySelector(".message-body").textContent,
-     "undefined",
-     "firstCall() returned correct value");
+  message = await onFirstCallMessageReceived;
+  ok(firstCallEvaluationResult !== unresolvedSymbol,
+    "firstCall() returned correct value");
 });
-
-function* executeAndConfirm(jsterm, input, output) {
-  info("Executing command `" + input + "`");
-
-  let node = yield jsterm.execute(input);
-
-  is(node.querySelector(".message-body").textContent, output,
-     "Expected result from call to " + input);
-}
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_execution_scope.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_execution_scope.js
@@ -3,35 +3,25 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that commands run by the user are executed in content space.
 
 "use strict";
 
 const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
-                 "test/test-console.html";
+                 "new-console-output/test/mochitest/test-console.html";
 
-add_task(function* () {
-  yield loadTab(TEST_URI);
-  let hud = yield openConsole();
-  hud.jsterm.clearOutput();
-  hud.jsterm.execute("window.location.href;");
+add_task(async function () {
+  const hud = await openNewTabAndConsole(TEST_URI);
+  const {jsterm} = hud;
+  jsterm.clearOutput();
 
-  let [input, output] = yield waitForMessages({
-    webconsole: hud,
-    messages: [{
-      text: "window.location.href;",
-      category: CATEGORY_INPUT,
-    },
-      {
-        text: TEST_URI,
-        category: CATEGORY_OUTPUT,
-      }],
-  });
+  const onInputMessage = waitForMessage(hud, "window.location.href;", ".message.command");
+  const onEvaluationResultMessage = waitForMessage(hud, TEST_URI, ".message.result");
+  jsterm.execute("window.location.href;");
 
-  let inputNode = [...input.matched][0];
-  let outputNode = [...output.matched][0];
-  is(inputNode.getAttribute("category"), "input",
-     "input node category is correct");
-  is(outputNode.getAttribute("category"), "output",
-     "output node category is correct");
+  let message = await onInputMessage;
+  ok(message, "Input message is displayed with the expected class");
+
+  message = await onEvaluationResultMessage;
+  ok(message, "EvaluationResult message is displayed with the expected class");
 });
--- a/devtools/client/webconsole/new-console-output/test/store/messages.test.js
+++ b/devtools/client/webconsole/new-console-output/test/store/messages.test.js
@@ -693,17 +693,17 @@ describe("Message reducer:", () => {
       const { dispatch, getState } = setupStore([
         "XHR GET request"
       ]);
 
       const updatePacket = stubPackets.get("XHR GET request update");
       dispatch(actions.networkMessageUpdate(updatePacket.networkInfo));
 
       let networkUpdates = getAllNetworkMessagesUpdateById(getState());
-      expect(Object.keys(networkUpdates).length).toBe(1);
+      expect(Object.keys(networkUpdates).length > 0).toBe(true);
 
       dispatch(actions.messagesClear());
 
       networkUpdates = getAllNetworkMessagesUpdateById(getState());
       expect(Object.keys(networkUpdates).length).toBe(0);
     });
 
     it("cleans the networkMessagesUpdateById property when messages are pruned", () => {
--- a/devtools/client/webconsole/new-console-output/test/store/ui.test.js
+++ b/devtools/client/webconsole/new-console-output/test/store/ui.test.js
@@ -13,26 +13,40 @@ describe("Testing UI", () => {
   let store;
 
   beforeEach(() => {
     store = setupStore();
   });
 
   describe("Toggle sidebar", () => {
     it("sidebar is toggled on and off", () => {
-      store.dispatch(actions.sidebarClose());
+      const packet = stubPackets.get("inspect({a: 1})");
+      const message = stubPreparedMessages.get("inspect({a: 1})");
+      store.dispatch(actions.messageAdd(packet));
+
+      const actorId = message.parameters[0].actor;
+      const messageId = getFirstMessage(store.getState()).id;
+      store.dispatch(actions.showObjectInSidebar(actorId, messageId));
+
       expect(store.getState().ui.sidebarVisible).toEqual(true);
       store.dispatch(actions.sidebarClose());
       expect(store.getState().ui.sidebarVisible).toEqual(false);
     });
   });
 
   describe("Hide sidebar on clear", () => {
     it("sidebar is hidden on clear", () => {
-      store.dispatch(actions.sidebarClose());
+      const packet = stubPackets.get("inspect({a: 1})");
+      const message = stubPreparedMessages.get("inspect({a: 1})");
+      store.dispatch(actions.messageAdd(packet));
+
+      const actorId = message.parameters[0].actor;
+      const messageId = getFirstMessage(store.getState()).id;
+      store.dispatch(actions.showObjectInSidebar(actorId, messageId));
+
       expect(store.getState().ui.sidebarVisible).toEqual(true);
       store.dispatch(actions.messagesClear());
       expect(store.getState().ui.sidebarVisible).toEqual(false);
       store.dispatch(actions.messagesClear());
       expect(store.getState().ui.sidebarVisible).toEqual(false);
     });
   });
 
--- a/devtools/docs/SUMMARY.md
+++ b/devtools/docs/SUMMARY.md
@@ -9,16 +9,17 @@
 * [Contributing](./contributing.md)
   * [Coding standards](./contributing/coding-standards.md)
     * [JavaScript](./contributing/javascript.md)
       * [ESLint](./contributing/eslint.md)
     * [CSS](./contributing/css.md)
   * [Creating and sending patches](./contributing/making-prs.md)
   * [Code reviews](./contributing/code-reviews.md)
   * [Filing good bugs](./contributing/filing-good-bugs.md)
+  * [Investigating performance issues](./contributing/performance.md)
 * [Bugs and issue trackers](bugs-issues.md)
 * [Files and directories](files/README.md)
   * [Adding New Files](files/adding-files.md)
 * [Tool Architectures](tools/tools.md)
   * [Inspector](tools/inspector.md)
     * [Panel Architecture](tools/inspector-panel.md)
     * [Highlighters](tools/highlighters.md)
   * [Memory](tools/memory-panel.md)
new file mode 100644
--- /dev/null
+++ b/devtools/docs/contributing/performance.md
@@ -0,0 +1,152 @@
+# Writing efficient code
+
+When debugging a page, tools get to slow down the website because of the added instrumentation.
+While working on Developer Tools we should strive to be the less impactful.
+First, because it is painful to work with laggy UI, but also because some tools record timings.
+For example, the network monitor records HTTP request timings.
+If the tools are slowing down Firefox significantly, it will make these measurements be wrong.
+
+To be efficient while working on performance, you should always focus on one precise user scenario.
+It could be:
+* a bug report where someone reports a precise interaction being slow,
+* or you could be trying to improve overall tools performance by looking at the most common usages.
+The important point here is to have some steps to reproduce, that you can redo manually in order to record a profile.
+And also, it is even better if you can replay via a test script. Test script that you can save as a new performance test.
+
+## Don't guess — profile.
+
+The very first thing to do is to record a profile while reproducing the scenario.
+
+Here's the Firefox documentation for [how to install the profiler and record a profile](https://developer.mozilla.org/docs/Mozilla/Performance/Reporting_a_Performance_Problem) and also [how to interpret the profiles](https://developer.mozilla.org/docs/Mozilla/Performance/Profiling_with_the_Built-in_Profiler#Understanding_Profiles)
+
+There are some peculiarities about DevTools architecture that are worth knowing about when looking at a profile:
+
+### Tweak profiler default settings
+
+The default buffer size (9MB) is too small. If you don't increase it, you may easily miss data and only see last couple of seconds of your recording.
+To increase the buffer size, click on the profiler add-on icon, in the Firefox toolbar, and set it to 360MB, like this:
+
+  <img src="performance/profiler-buffer-size.png" alt="Profiler buffer size" style="width: 300px" />
+
+The other setting worth mentioning for DevTools debugging is the `Interval`
+The profiler records only samples, based on this `Interval`.
+If you want to see more fine-grained stack traces, you may reduce this interval to 0.1ms,
+but do that only if you really need it, as it will make Firefox much even slower when recording,
+and the measured times will be even slower.
+
+### The DevTools UI runs on the parent process
+
+When you are debugging tool front-ends (e.g. panels), always ensure you select the `Main Thread` line.
+It should have a light blue background like this:
+
+  <img src="performance/profiler-main-thread.png" alt="Select main process" style="width: 300px" />
+
+Otherwise, the vast majority of DevTools backend (DebuggerServer, actors, ...) lives in content processes.
+So if you are debugging them, you should select one of the `Content` lines.
+
+### Most of the DevTools codebase is in Javascript
+
+In the call tree, it is easier to filter by `JS`, via this menu list:
+  <img src="performance/profiler-filter-js.png" alt="JS Filtering" style="width: 200px" />
+
+But note that you may have to switch back to `Combined` in order to understand why some particular Javascript method is slow.
+
+### Handy filter strings for DevTools:
+
+  * `require`
+    Helps highlighting the cost of module loading
+     ![modules](performance/profiler-filter-require.png)
+  * DevTools uses two kind of URLs:
+    * `chrome://devtools/` for all panel documents. Filter with this to see the cost of all panel documents:
+      ![panels documents](performance/profiler-chrome-url.png)
+    * `resource://devtools/` for all javascript modules. Filter with this to see the cost of all modules:
+      ![modules](performance/profiler-resource-url.png)
+
+### Record durations manually
+
+Sometimes it is handy to focus on a very precise piece of code and record its time manually.
+For example when you identified one slow running method and think you can speed it up.
+It saves your from having to: record the profile, wait for the profiler to display and search for the precise method durations.
+
+#### Print durations in your Terminal and in the Browser Console
+
+You can use the [`Performance`](https://developer.mozilla.org/docs/Web/API/Performance) API, like this:
+```
+let start = window.performance.now();
+
+// Run the code you want to measure
+
+// Once it is done, do:
+console.log("my function took", window.performance.now() - start, "ms");
+```
+
+#### Use markers
+
+The Performance API also allows recording markers, like this:
+```
+window.performance.mark("my-function-start");
+
+// Run the code you want to measure
+
+// Once it is done, do:
+window.performance.measure("my-function", "my-function-start");
+```
+
+This marker will appear in the `Marker Chart` section in perf-html, in the `UserTiming` lines:
+  ![custom markers](performance/profiler-custom-markers.png)
+
+You can double click on it to make perf-html display the record during this precise moment in time,
+and the call tree will only display what was executed during this measurement.
+
+### Prototype quickly
+
+Sometimes the best way to find what is slow is to comment blocks of code out
+and uncomment them one by one until you identify the culprit. And then focus on it.
+
+There are few things worse than spending a long time refactoring the piece of code that was not slow to begin with!
+
+## Assess your improvement.
+
+Once you have a patch that you think improves the performance, you have to assess whether it actually improves it.
+
+### Record another profile
+
+Compare the two profiles, without and with your patch.
+Then see if the call tree reports a significant difference:
+* A function call completely disappears in the new profile, with your fix.
+  For example you were loading a big module, and you got a frame for `require("my/big/module")` call, and no longer see it.
+* The same function call takes xxx ms less with your patch.
+
+This [lazy loading of modules in netmonitor](https://bugzilla.mozilla.org/show_bug.cgi?id=1420289) is a good example.
+Without this patch, App.js was loading in 91ms and was loading MonitorPanel.js and StatisticsPanel.js as dependencies:
+  ![netmonitor without patch](performance/profiler-netmonitor-open.png)
+
+With the patch, App.js loads in 47ms and only loads MonitorPanel.js:
+  ![netmonitor with patch](performance/profiler-netmon-open-fixed.png)
+
+It highlights that:
+ * we no longer load StatisticsPanel,
+ * App is faster to load.
+
+### Run performance tests
+
+See if any subtest reports a improvement. Ensure that the improvement makes any sense.
+For example, if the test is 50% faster, maybe you broke the performance test.
+This might happen if the test no longer waits for all the operations to finish executing before completing.
+
+To push your current patch to try, execute:
+```
+./mach try -b o -p linux64 -u none -t g2-e10s --rebuild-talos 5 --artifact
+```
+It will print in your Terminal a link to perfherder like this one:
+[https://treeherder.mozilla.org/perf.html#/comparechooser?newProject=try&newRevision=9bef6cb13c43bbce21d40ffaea595e082a4c28db](https://treeherder.mozilla.org/perf.html#/comparechooser?newProject=try&newRevision=9bef6cb13c43bbce21d40ffaea595e082a4c28db)
+Running performance tests takes time, so you should open it 30 minutes up to 2 hours later to see your results.
+See [Performance tests (DAMP)](../tests/performance-tests.md) for more information about PerfHerder/try.
+
+Let's look at how to interpret an actual real-life [set of perfherder results](https://treeherder.mozilla.org/perf.html#/comparesubtest?originalProject=mozilla-central&newProject=try&newRevision=9bef6cb13c43bbce21d40ffaea595e082a4c28db&originalSignature=edaec66500db21d37602c99daa61ac983f21a6ac&newSignature=edaec66500db21d37602c99daa61ac983f21a6ac&showOnlyImportant=1&framework=1&selectedTimeRange=172800):
+
+![perfherder results](performance/perfherder-results.png)
+
+These results are related to [lazy loading of modules in netmonitor](https://bugzilla.mozilla.org/show_bug.cgi?id=1420289).
+It is interesting to see that this patch is a trade-off. It makes netmonitor opening significantly faster, by preventing loading many modules during its opening.
+But it makes the page reload a bit slower as some modules that used to be loaded during netmonitor open, now have to be loaded during page reload.
--- a/devtools/server/actors/stylesheets.js
+++ b/devtools/server/actors/stylesheets.js
@@ -418,16 +418,22 @@ var StyleSheetActor = protocol.ActorClas
    *         The href of the stylesheet to retrieve.
    * @return {Promise} a promise that resolves with an object with the following members
    *         on success:
    *           - content: the document at that URL, as a string,
    *           - contentType: the content type of the document
    *         If an error occurs, the promise is rejected with that error.
    */
   fetchStylesheet: Task.async(function* (href) {
+    // Check if network monitor observed this load, and if so, use that.
+    let result = this.fetchStylesheetFromNetworkMonitor(href);
+    if (result) {
+      return result;
+    }
+
     let options = {
       loadFromCache: true,
       policy: Ci.nsIContentPolicy.TYPE_INTERNAL_STYLESHEET,
       charset: this._getCSSCharset()
     };
 
     // Bug 1282660 - We use the system principal to load the default internal
     // stylesheets instead of the content principal since such stylesheets
@@ -437,33 +443,69 @@ var StyleSheetActor = protocol.ActorClas
     // chrome|file|resource|moz-extension protocols rely on the system principal.
     let excludedProtocolsRe = /^(chrome|file|resource|moz-extension):\/\//;
     if (!excludedProtocolsRe.test(this.href)) {
       // Stylesheets using other protocols should use the content principal.
       options.window = this.ownerWindow;
       options.principal = this.ownerDocument.nodePrincipal;
     }
 
-    let result;
     try {
       result = yield fetch(this.href, options);
     } catch (e) {
       // The list of excluded protocols can be missing some protocols, try to use the
       // system principal if the first fetch failed.
       console.error(`stylesheets actor: fetch failed for ${this.href},` +
         ` using system principal instead.`);
       options.window = undefined;
       options.principal = undefined;
       result = yield fetch(this.href, options);
     }
 
     return result;
   }),
 
   /**
+   * Try to locate the console actor if it exists via our parent actor (the tab).
+   */
+  get _consoleActor() {
+    if (this.parentActor.exited) {
+      return null;
+    }
+    let form = this.parentActor.form();
+    return this.conn._getOrCreateActor(form.consoleActor);
+  },
+
+  /**
+   * Try to fetch the stylesheet text from the network monitor.  If it was enabled during
+   * the load, it should have a copy of the text saved.
+   *
+   * @param string href
+   *        The URL of the sheet to fetch.
+   */
+  fetchStylesheetFromNetworkMonitor(href) {
+    let consoleActor = this._consoleActor;
+    if (!consoleActor) {
+      return null;
+    }
+    let request = consoleActor.getNetworkEventActorForURL(href);
+    if (!request) {
+      return null;
+    }
+    let content = request._response.content;
+    if (request._discardResponseBody || !content) {
+      return null;
+    }
+    return {
+      content: content.text,
+      contentType: content.mimeType,
+    };
+  },
+
+  /**
    * Protocol method to get the media rules for the stylesheet.
    */
   getMediaRules: function () {
     return this._getMediaRules();
   },
 
   /**
    * Get all the @media rules in this stylesheet.
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -63,16 +63,17 @@ function WebConsoleActor(connection, par
   this._actorPool = new ActorPool(this.conn);
   this.conn.addActorPool(this._actorPool);
 
   this._prefs = {};
 
   this.dbg = this.parentActor.makeDebugger();
 
   this._netEvents = new Map();
+  this._networkEventActorsByURL = new Map();
   this._gripDepth = 0;
   this._listeners = new Set();
   this._lastConsoleInputEvaluation = undefined;
 
   this.objectGrip = this.objectGrip.bind(this);
   this._onWillNavigate = this._onWillNavigate.bind(this);
   this._onChangedToplevelDocument = this._onChangedToplevelDocument.bind(this);
   EventEmitter.on(this.parentActor, "changed-toplevel-document",
@@ -119,24 +120,34 @@ WebConsoleActor.prototype =
    * Web Console-related preferences.
    * @private
    * @type object
    */
   _prefs: null,
 
   /**
    * Holds a map between nsIChannel objects and NetworkEventActors for requests
-   * created with sendHTTPRequest.
+   * created with sendHTTPRequest or found via the network listener.
    *
    * @private
    * @type Map
    */
   _netEvents: null,
 
   /**
+   * Holds a map from URL to NetworkEventActors for requests noticed by the network
+   * listener.  Requests are added when they start, so the actor might not yet have all
+   * data for the request until it has completed.
+   *
+   * @private
+   * @type Map
+   */
+  _networkEventActorsByURL: null,
+
+  /**
    * Holds a set of all currently registered listeners.
    *
    * @private
    * @type Set
    */
   _listeners: null,
 
   /**
@@ -1627,16 +1638,18 @@ WebConsoleActor.prototype =
    * @return object
    *         A new NetworkEventActor is returned. This is used for tracking the
    *         network request and response.
    */
   onNetworkEvent: function (event) {
     let actor = this.getNetworkEventActor(event.channelId);
     actor.init(event);
 
+    this._networkEventActorsByURL.set(actor._request.url, actor);
+
     let packet = {
       from: this.actorID,
       type: "networkEvent",
       eventActor: actor.grip()
     };
 
     this.conn.send(packet);
 
@@ -1661,16 +1674,28 @@ WebConsoleActor.prototype =
     }
 
     actor = new NetworkEventActor(this);
     this._actorPool.addActor(actor);
     return actor;
   },
 
   /**
+   * Get the NetworkEventActor for a given URL that may have been noticed by the network
+   * listener.  Requests are added when they start, so the actor might not yet have all
+   * data for the request until it has completed.
+   *
+   * @param string url
+   *        The URL of the request to search for.
+   */
+  getNetworkEventActorForURL(url) {
+    return this._networkEventActorsByURL.get(url);
+  },
+
+  /**
    * Send a new HTTP request from the target's window.
    *
    * @param object message
    *        Object with 'request' - the HTTP request details.
    */
   onSendHTTPRequest(message) {
     let { url, method, headers, body } = message.request;
 
@@ -1987,16 +2012,19 @@ NetworkEventActor.prototype =
     for (let grip of this._longStringActors) {
       let actor = this.parent.getActorByID(grip.actor);
       if (actor) {
         this.parent.releaseActor(actor);
       }
     }
     this._longStringActors = new Set();
 
+    if (this._request.url) {
+      this.parent._networkEventActorsByURL.delete(this._request.url);
+    }
     if (this.channel) {
       this.parent._netEvents.delete(this.channel);
     }
     this.parent.releaseActor(this);
   },
 
   /**
    * Handle a protocol request to release a grip.
--- a/dom/base/Element.cpp
+++ b/dom/base/Element.cpp
@@ -2680,17 +2680,17 @@ Element::SetAttrAndNotify(int32_t aNames
                           const mozAutoDocUpdate&)
 {
   nsresult rv;
   nsMutationGuard::DidMutate();
 
   // Copy aParsedValue for later use since it will be lost when we call
   // SetAndSwapMappedAttr below
   nsAttrValue valueForAfterSetAttr;
-  if (aCallAfterSetAttr) {
+  if (aCallAfterSetAttr || GetCustomElementData()) {
     valueForAfterSetAttr.SetTo(aParsedValue);
   }
 
   bool hadValidDir = false;
   bool hadDirAuto = false;
   bool oldValueSet;
 
   if (aNamespaceID == kNameSpaceID_None) {
--- a/dom/base/nsStyledElement.cpp
+++ b/dom/base/nsStyledElement.cpp
@@ -81,17 +81,17 @@ nsStyledElement::SetInlineStyleDeclarati
     nsContentUtils::HasMutationListeners(this,
                                          NS_EVENT_BITS_MUTATION_ATTRMODIFIED,
                                          this);
 
   // There's no point in comparing the stylerule pointers since we're always
   // getting a new stylerule here. And we can't compare the stringvalues of
   // the old and the new rules since both will point to the same declaration
   // and thus will be the same.
-  if (hasListeners) {
+  if (hasListeners || GetCustomElementData()) {
     // save the old attribute so we can set up the mutation event properly
     nsAutoString oldValueStr;
     modification = GetAttr(kNameSpaceID_None, nsGkAtoms::style,
                            oldValueStr);
     if (modification) {
       oldValue.SetTo(oldValueStr);
       oldValueSet = true;
     }
--- a/dom/bindings/GenerateCSS2PropertiesWebIDL.py
+++ b/dom/bindings/GenerateCSS2PropertiesWebIDL.py
@@ -23,17 +23,17 @@ def generate(output, idlFilename, prepro
 
     propList = eval(preprocessed)
     props = ""
     for [name, prop, id, flags, pref, proptype] in propList:
         if "CSS_PROPERTY_INTERNAL" in flags:
             continue
         # Unfortunately, even some of the getters here are fallible
         # (e.g. on nsComputedDOMStyle).
-        extendedAttrs = ["Throws", "TreatNullAs=EmptyString",
+        extendedAttrs = ["CEReactions", "Throws", "TreatNullAs=EmptyString",
                          "SetterNeedsSubjectPrincipal=NonSystem"]
         if pref is not "":
             extendedAttrs.append('Pref="%s"' % pref)
 
         # webkit properties get a capitalized "WebkitFoo" accessor (added here)
         # as well as a camelcase "webkitFoo" accessor (added next).
         if (prop.startswith("Webkit")):
             props += generateLine(prop, extendedAttrs)
--- a/dom/media/ogg/OggDemuxer.cpp
+++ b/dom/media/ogg/OggDemuxer.cpp
@@ -1359,16 +1359,17 @@ OggTrackDemuxer::NextSample()
   // mDecodedAudioDuration gets adjusted during ReadOggChain().
   TimeUnit totalDuration = mParent->mDecodedAudioDuration;
   if (eos) {
     // We've encountered an end of bitstream packet; check for a chained
     // bitstream following this one.
     // This will also update mSharedAudioTrackInfo.
     mParent->ReadOggChain(data->GetEndTime());
   }
+  data->mOffset = mParent->Resource(mType)->Tell();
   // We adjust the start time of the sample to account for the potential ogg chaining.
   data->mTime += totalDuration;
   return data;
 }
 
 RefPtr<OggTrackDemuxer::SamplesPromise>
 OggTrackDemuxer::GetSamples(int32_t aNumSamples)
 {
--- a/dom/tests/mochitest/webcomponents/test_custom_element_lifecycle.html
+++ b/dom/tests/mochitest/webcomponents/test_custom_element_lifecycle.html
@@ -287,16 +287,58 @@ function testAttributeChangedExtended() 
 
   customElements.define("x-extended-attribute-change", ExtnededAttributeChange,
                         { extends: "button" });
 
   var elem = document.createElement("button", {is: "x-extended-attribute-change"});
   elem.setAttribute("foo", "bar");
 }
 
+function testStyleAttributeChange() {
+  var expectedCallbackArguments = [
+    // [name, oldValue, newValue]
+    // This is an additional attributeChangedCallback from *first* style
+    // attribute change, see https://bugzilla.mozilla.org/show_bug.cgi?id=1428246.
+    ["style", null, ""],
+    ["style", "", "font-size: 10px;"],
+    ["style", "font-size: 10px;", "font-size: 20px;"],
+    ["style", "font-size: 20px;", "font-size: 30px;"],
+  ];
+
+  customElements.define("x-style-attribute-change", class extends HTMLElement {
+    attributeChangedCallback(name, oldValue, newValue) {
+      if (expectedCallbackArguments.length === 0) {
+        ok(false, "Got unexpected attributeChangedCallback?");
+        return;
+      }
+
+      let expectedArgument = expectedCallbackArguments.shift();
+      is(name, expectedArgument[0],
+         "The name argument should match the expected value.");
+      is(oldValue, expectedArgument[1],
+         "The old value argument should match the expected value.");
+      is(newValue, expectedArgument[2],
+         "The new value argument should match the expected value.");
+    }
+
+    static get observedAttributes() {
+      return ["style"];
+    }
+  });
+
+  var elem = document.createElement("x-style-attribute-change");
+  elem.style.fontSize = "10px";
+  elem.style.fontSize = "20px";
+  elem.style.fontSize = "30px";
+
+  ok(expectedCallbackArguments.length === 0,
+     "The attributeChangedCallback should be fired synchronously.");
+  runNextTest();
+}
+
 // Creates a custom element that is an upgrade candidate (no registration)
 // and mutate the element in ways that would call callbacks for registered
 // elements.
 function testUpgradeCandidate() {
   var createdElement = document.createElement("x-upgrade-candidate");
   container.appendChild(createdElement);
   createdElement.setAttribute("foo", "bar");
   container.removeChild(createdElement);
@@ -363,16 +405,17 @@ var testFunctions = [
   testRegisterUnresolvedExtended,
   testInnerHTML,
   testInnerHTMLExtended,
   testInnerHTMLUpgrade,
   testInnerHTMLExtendedUpgrade,
   testRegisterResolved,
   testAttributeChanged,
   testAttributeChangedExtended,
+  testStyleAttributeChange,
   testUpgradeCandidate,
   testChangingCallback,
   testNotInDocEnterLeave,
   testEnterLeaveView,
   SimpleTest.finish
 ];
 
 function runNextTest() {
--- a/layout/base/ServoRestyleManager.cpp
+++ b/layout/base/ServoRestyleManager.cpp
@@ -102,17 +102,19 @@ ExpectedOwnerForChild(const nsIFrame& aF
   while (parent && (IsAnonBox(*parent) || parent->IsLineFrame())) {
     auto* pseudo = parent->StyleContext()->GetPseudo();
     if (pseudo == nsCSSAnonBoxes::tableWrapper) {
       const nsIFrame* tableFrame = parent->PrincipalChildList().FirstChild();
       MOZ_ASSERT(tableFrame->IsTableFrame());
       // Handle :-moz-table and :-moz-inline-table.
       parent = IsAnonBox(*tableFrame) ? parent->GetParent() : tableFrame;
     } else {
-      parent = parent->GetParent();
+      // We get the in-flow parent here so that we can handle the OOF anonymous
+      // boxed to get the correct parent.
+      parent = parent->GetInFlowParent();
     }
     parent = FirstContinuationOrPartOfIBSplit(parent);
   }
 
   return parent;
 }
 
 void
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -1094,25 +1094,31 @@ GetDisplayPortFromMarginsData(nsIContent
       MAX_ALIGN_ROUNDING * alignment.height;
 
     // For each axis, inflate the margins up to the maximum size.
     const ScreenMargin& margins = aMarginsData->mMargins;
     if (screenRect.height < maxHeightScreenPx) {
       int32_t budget = maxHeightScreenPx - screenRect.height;
       // Scale the margins down to fit into the budget if necessary, maintaining
       // their relative ratio.
-      float scale = std::min(1.0f, float(budget) / margins.TopBottom());
+      float scale = 1.0f;
+      if (float(budget) < margins.TopBottom()) {
+        scale = float(budget) / margins.TopBottom();
+      }
       float top = margins.top * scale;
       float bottom = margins.bottom * scale;
       screenRect.y -= top;
       screenRect.height += top + bottom;
     }
     if (screenRect.width < maxWidthScreenPx) {
       int32_t budget = maxWidthScreenPx - screenRect.width;
-      float scale = std::min(1.0f, float(budget) / margins.LeftRight());
+      float scale = 1.0f;
+      if (float(budget) < margins.LeftRight()) {
+        scale = float(budget) / margins.LeftRight();
+      }
       float left = margins.left * scale;
       float right = margins.right * scale;
       screenRect.x -= left;
       screenRect.width += left + right;
     }
   }
 
   ScreenPoint scrollPosScreen = LayoutDevicePoint::FromAppUnits(scrollPos, auPerDevPixel)
--- a/layout/generic/nsIFrame.h
+++ b/layout/generic/nsIFrame.h
@@ -883,17 +883,17 @@ public:
    * Accessor functions for geometric parent.
    */
   nsContainerFrame* GetParent() const { return mParent; }
 
   /**
    * Gets the parent of a frame, using the parent of the placeholder for
    * out-of-flow frames.
    */
-  inline nsContainerFrame* GetInFlowParent();
+  inline nsContainerFrame* GetInFlowParent() const;
 
   /**
    * Gets the primary frame of the Content's flattened tree
    * parent, if one exists.
    */
   nsIFrame* GetFlattenedTreeParentPrimaryFrame() const;
 
   /**
--- a/layout/generic/nsIFrameInlines.h
+++ b/layout/generic/nsIFrameInlines.h
@@ -165,17 +165,17 @@ nsIFrame::PropagateRootElementWritingMod
 {
   MOZ_ASSERT(IsCanvasFrame());
   for (auto f = this; f; f = f->GetParent()) {
     f->mWritingMode = aRootElemWM;
   }
 }
 
 nsContainerFrame*
-nsIFrame::GetInFlowParent()
+nsIFrame::GetInFlowParent() const
 {
   if (GetStateBits() & NS_FRAME_OUT_OF_FLOW) {
     nsIFrame* ph = FirstContinuation()->GetProperty(nsIFrame::PlaceholderFrameProperty());
     return ph->GetParent();
   }
 
   return GetParent();
 }
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -1781,16 +1781,17 @@ public abstract class GeckoApp extends G
                     if (isFirstTab) {
                         flags |= Tabs.LOADURL_FIRST_AFTER_ACTIVITY_UNHIDDEN;
                     }
                     Tabs.getInstance().loadUrlWithIntentExtras(url, intent, flags);
                 }
             });
         } else if (Intent.ACTION_ASSIST.equals(action)) {
             Tabs.getInstance().addTab(Tabs.LOADURL_START_EDITING | Tabs.LOADURL_EXTERNAL);
+            autoHideTabs();
         } else if (ACTION_HOMESCREEN_SHORTCUT.equals(action)) {
             final GeckoBundle data = new GeckoBundle(2);
             data.putString("uri", uri);
             data.putString("flags", "OPEN_SWITCHTAB");
             getAppEventDispatcher().dispatch("Tab:OpenUri", data);
         } else if (Intent.ACTION_SEARCH.equals(action)) {
             final GeckoBundle data = new GeckoBundle(2);
             data.putString("uri", uri);
--- a/servo/components/script/script_runtime.rs
+++ b/servo/components/script/script_runtime.rs
@@ -255,17 +255,16 @@ pub unsafe fn new_rt_and_cx() -> Runtime
     if let Some(val) = PREFS.get("js.mem.gc.high_frequency_time_limit_ms").as_i64() {
         if val >= 0 && val < 10000 {
             JS_SetGCParameter(runtime.rt(), JSGCParamKey::JSGC_HIGH_FREQUENCY_TIME_LIMIT, val as u32);
         }
     }
     if let Some(val) = PREFS.get("js.mem.gc.dynamic_mark_slice.enabled").as_boolean() {
         JS_SetGCParameter(runtime.rt(), JSGCParamKey::JSGC_DYNAMIC_MARK_SLICE, val as u32);
     }
-    // TODO: handle js.mem.gc.refresh_frame_slices.enabled
     if let Some(val) = PREFS.get("js.mem.gc.dynamic_heap_growth.enabled").as_boolean() {
         JS_SetGCParameter(runtime.rt(), JSGCParamKey::JSGC_DYNAMIC_HEAP_GROWTH, val as u32);
     }
     if let Some(val) = PREFS.get("js.mem.gc.low_frequency_heap_growth").as_i64() {
         if val >= 0 && val < 10000 {
             JS_SetGCParameter(runtime.rt(), JSGCParamKey::JSGC_LOW_FREQUENCY_HEAP_GROWTH, val as u32);
         }
     }
--- a/servo/resources/prefs.json
+++ b/servo/resources/prefs.json
@@ -40,17 +40,16 @@
   "js.mem.gc.high_frequency_heap_growth_min": 150,
   "js.mem.gc.high_frequency_high_limit_mb": 500,
   "js.mem.gc.high_frequency_low_limit_mb": 100,
   "js.mem.gc.high_frequency_time_limit_ms": 1000,
   "js.mem.gc.incremental.enabled": true,
   "js.mem.gc.incremental.slice_ms": 10,
   "js.mem.gc.low_frequency_heap_growth": 150,
   "js.mem.gc.per_compartment.enabled": true,
-  "js.mem.gc.refresh_frame_slices.enabled": true,
   "js.mem.gc.zeal.frequency": 100,
   "js.mem.gc.zeal.level": 0,
   "js.mem.high_water_mark": 128,
   "js.mem.max": -1,
   "js.native_regexp.enabled": true,
   "js.parallel_parsing.enabled": true,
   "js.shared_memory.enabled": true,
   "js.strict.debug.enabled": false,
--- a/taskcluster/ci/docker-image/kind.yml
+++ b/taskcluster/ci/docker-image/kind.yml
@@ -30,16 +30,18 @@ jobs:
   lint:
     symbol: I(lnt)
   android-build:
     symbol: I(agb)
   index-task:
     symbol: I(idx)
   funsize-update-generator:
     symbol: I(pg)
+  google-play-strings:
+    symbol: I(gps)
   funsize-balrog-submitter:
     symbol: I(fbs)
   beet-mover:
     symbol: I(bm)
   update-verify:
     symbol: I(uv)
   diffoscope:
     symbol: I(diff)
new file mode 100644
--- /dev/null
+++ b/taskcluster/ci/google-play-strings/kind.yml
@@ -0,0 +1,54 @@
+# 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/.
+
+loader: taskgraph.loader.transform:loader
+
+transforms:
+   - taskgraph.transforms.google_play_strings:transforms
+   - taskgraph.transforms.task:transforms
+
+jobs:
+   google-play-strings:
+      description: Download strings to display on Google Play from https://l10n.mozilla-community.org/stores_l10n/
+      attributes:
+         build_type: google_play_strings
+         build_platform: android-nightly
+         nightly: true
+      shipping-phase: promote
+      shipping-product: fennec
+      worker-type: aws-provisioner-v1/gecko-{level}-b-android
+      worker:
+         implementation: docker-worker
+         os: linux
+         docker-image: {in-tree: google-play-strings}
+         chain-of-trust: true
+         max-run-time: 600
+         artifacts:
+            - name: 'public/google_play_strings.json'
+              # XXX The folder depends on the one defined in the Dockerfile
+              path: /builds/worker/google_play_strings.json
+              type: 'file'
+         env:
+            # TODO Use the branch name instead of the android package name
+            PACKAGE_NAME:
+               by-project:
+                  mozilla-central: org.mozilla.fennec_aurora
+                  mozilla-beta: org.mozilla.firefox_beta
+                  mozilla-release: org.mozilla.firefox_beta
+                  default: org.mozilla.fennec_aurora  # Fetches strings for mozilla-central
+            # XXX The folder depends on the one defined in the Dockerfile
+            GOOGLE_PLAY_STRING_FILE: /builds/worker/google_play_strings.json
+         command:
+            - bash
+            - -cx
+            - >
+              python3 ./mozapkpublisher/get_l10n_strings.py
+              --package-name "${PACKAGE_NAME}"
+              --output-file "${GOOGLE_PLAY_STRING_FILE}"
+      treeherder:
+         symbol: pub(gps)
+         platform: Android/opt
+         tier: 2
+         kind: other
+      run-on-projects: ['maple', 'mozilla-central', 'mozilla-beta', 'mozilla-release']
--- a/taskcluster/ci/push-apk/kind.yml
+++ b/taskcluster/ci/push-apk/kind.yml
@@ -1,43 +1,43 @@
 # 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/.
 
 loader: taskgraph.loader.push_apk:loader
 
 transforms:
-    - taskgraph.transforms.push_apk:transforms
-    - taskgraph.transforms.task:transforms
+   - taskgraph.transforms.push_apk:transforms
+   - taskgraph.transforms.task:transforms
 
 kind-dependencies:
-    - build-signing
-    - push-apk-breakpoint
+   - build-signing
+   - google-play-strings
+   - push-apk-breakpoint
 
 jobs:
-    push-apk/opt:
-        description: Publishes APK onto Google Play Store
-        attributes:
-            build_platform: android-nightly
-            nightly: true
-        shipping-phase: ship
-        shipping-product: fennec
-        worker-type:
-            by-project:
-                mozilla-central: scriptworker-prov-v1/pushapk-v1
-                mozilla-beta: scriptworker-prov-v1/pushapk-v1
-                mozilla-release: scriptworker-prov-v1/pushapk-v1
-                default: scriptworker-prov-v1/dep-pushapk
-        worker:
-            upstream-artifacts:  # see transforms
-            google-play-track:  # see transforms
-            implementation: push-apk
-            commit:  # see transforms
-        scopes:  # see transforms
-        treeherder:
-            symbol: pub(gp)
-            platform: Android/opt
-            tier: 2
-            kind: other
-        run-on-projects: ['mozilla-central', 'mozilla-beta', 'mozilla-release']
-        deadline-after: 5 days
-        extra:
-            product: fennec
+   push-apk/opt:
+      description: Publishes APK onto Google Play Store
+      attributes:
+         build_platform: android-nightly
+         nightly: true
+      shipping-phase: ship
+      shipping-product: fennec
+      worker-type:
+         by-project:
+            mozilla-central: scriptworker-prov-v1/pushapk-v1
+            mozilla-beta: scriptworker-prov-v1/pushapk-v1
+            mozilla-release: scriptworker-prov-v1/pushapk-v1
+            default: scriptworker-prov-v1/dep-pushapk
+      worker:
+         upstream-artifacts:  # see transforms
+         google-play-track:  # see transforms
+         implementation: push-apk
+         commit:  # see transforms
+      requires: all-resolved
+      scopes:  # see transforms
+      treeherder:
+         symbol: pub(gp)
+         platform: Android/opt
+         tier: 2
+         kind: other
+      run-on-projects: ['mozilla-central', 'mozilla-beta', 'mozilla-release', 'maple']
+      deadline-after: 5 days
--- a/taskcluster/docker/firefox-snap/snapcraft.yaml.in
+++ b/taskcluster/docker/firefox-snap/snapcraft.yaml.in
@@ -1,23 +1,26 @@
 name: firefox
 version: @VERSION@-@BUILD_NUMBER@
 summary: Mozilla Firefox web browser
 description:  Firefox is a powerful, extensible web browser with support for modern web application technologies.
 confinement: strict
+grade: stable
 
 apps:
   firefox:
     command: desktop-launch firefox
     desktop: opt/firefox/distribution/firefox.desktop
     environment:
       DISABLE_WAYLAND: 1
     plugs:
+      - avahi-observe
       - browser-sandbox
       - camera
+      - cups-control
       - desktop
       - desktop-legacy
       - gsettings
       - home
       - network
       - opengl
       - pulseaudio
       - screen-inhibit-control
@@ -36,9 +39,12 @@ parts:
     stage-packages:
       - libxt6
       - libdbus-glib-1-2
       - libasound2
       - libpulse0
       - libgl1-mesa-dri
       - libgl1-mesa-glx
       - libmirclient9
+      - desktop-file-utils
+      - xdg-utils
+      - ffmpeg
     after: [desktop-gtk3]
--- a/taskcluster/docker/funsize-update-generator/requirements.txt
+++ b/taskcluster/docker/funsize-update-generator/requirements.txt
@@ -1,5 +1,7 @@
 mar==2.1.2
 backports.lzma==0.0.8
 datadog==0.17.0
 redo==1.6
+aiohttp==2.3.6
 awscli==1.14.10
+scriptworker==6.0.0
--- a/taskcluster/docker/funsize-update-generator/runme.sh
+++ b/taskcluster/docker/funsize-update-generator/runme.sh
@@ -13,17 +13,18 @@ curl --location --retry 10 --retry-delay
     "https://queue.taskcluster.net/v1/task/$TASK_ID"
 
 # auth:aws-s3:read-write:tc-gp-private-1d-us-east-1/releng/mbsdiff-cache/
 # -> bucket of tc-gp-private-1d-us-east-1, path of releng/mbsdiff-cache/
 # Trailing slash is important, due to prefix permissions in S3.
 S3_BUCKET_AND_PATH=$(jq -r '.scopes[] | select(contains ("auth:aws-s3"))' /home/worker/task.json | awk -F: '{print $4}')
 
 # Will be empty if there's no scope for AWS S3.
-if [ -n "${S3_BUCKET_AND_PATH}" ]; then
+if [ -n "${S3_BUCKET_AND_PATH}" ] && getent hosts taskcluster
+then
   # Does this parse as we expect?
   S3_PATH=${S3_BUCKET_AND_PATH#*/}
   AWS_BUCKET_NAME=${S3_BUCKET_AND_PATH%/${S3_PATH}*}
   test "${S3_PATH}"
   test "${AWS_BUCKET_NAME}"
 
   set +x  # Don't echo these.
   secret_url="taskcluster/auth/v1/aws/s3/read-write/${AWS_BUCKET_NAME}/${S3_PATH}"
--- a/taskcluster/docker/funsize-update-generator/scripts/funsize.py
+++ b/taskcluster/docker/funsize-update-generator/scripts/funsize.py
@@ -1,35 +1,44 @@
 #!/usr/bin/env python3
 # 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, division, print_function
 
+import asyncio
+import aiohttp
 import configparser
 import argparse
 import hashlib
 import json
 import logging
 import os
 import shutil
 import tempfile
 import time
 import requests
 import sh
 
 import redo
+from scriptworker.utils import retry_async
 from mardor.reader import MarReader
 from mardor.signing import get_keysize
 
 from datadog import initialize, ThreadStats
 
 
 log = logging.getLogger(__name__)
+
+# Create this even when not sending metrics, so the context manager
+# statements work.
+ddstats = ThreadStats(namespace='releng.releases.partials')
+
+
 ALLOWED_URL_PREFIXES = [
     "http://download.cdn.mozilla.net/pub/mozilla.org/firefox/nightly/",
     "http://download.cdn.mozilla.net/pub/firefox/nightly/",
     "https://mozilla-nightly-updates.s3.amazonaws.com",
     "https://queue.taskcluster.net/",
     "http://ftp.mozilla.org/",
     "http://download.mozilla.org/",
     "https://archive.mozilla.org/",
@@ -66,131 +75,178 @@ def get_secret(secret_name):
     # 403: If unauthorized, just give up.
     if r.status_code == 403:
         log.info("Unable to get secret key")
         return {}
     r.raise_for_status()
     return r.json().get('secret', {})
 
 
-@redo.retriable()
-def download(url, dest, mode=None):
-    log.debug("Downloading %s to %s", url, dest)
-    r = requests.get(url)
-    r.raise_for_status()
+async def retry_download(*args, **kwargs):  # noqa: E999
+    """Retry download() calls."""
+    await retry_async(
+        download,
+        retry_exceptions=(
+            aiohttp.ClientError
+        ),
+        args=args,
+        kwargs=kwargs
+    )
+
+
+async def download(url, dest, mode=None):  # noqa: E999
+    log.info("Downloading %s to %s", url, dest)
 
     bytes_downloaded = 0
-    with open(dest, 'wb') as fd:
-        for chunk in r.iter_content(4096):
-            fd.write(chunk)
-            bytes_downloaded += len(chunk)
 
-    log.debug('Downloaded %s bytes', bytes_downloaded)
-    if 'content-length' in r.headers:
-        log.debug('Content-Length: %s bytes', r.headers['content-length'])
-        if bytes_downloaded != int(r.headers['content-length']):
-            raise IOError('Unexpected number of bytes downloaded')
+    async with aiohttp.ClientSession(raise_for_status=True) as session:
+        async with session.get(url) as resp:
+            with open(dest, 'wb') as fd:
+                while True:
+                    chunk = await resp.content.read(4096)
+                    if not chunk:
+                        break
+                    fd.write(chunk)
+                    bytes_downloaded += len(chunk)
 
-    if mode:
-        log.debug("chmod %o %s", mode, dest)
-        os.chmod(dest, mode)
+            log.debug('Downloaded %s bytes', bytes_downloaded)
+            if 'content-length' in resp.headers:
+                log.debug('Content-Length: %s bytes', resp.headers['content-length'])
+                if bytes_downloaded != int(resp.headers['content-length']):
+                    raise IOError('Unexpected number of bytes downloaded')
+
+            if mode:
+                log.debug("chmod %o %s", mode, dest)
+                os.chmod(dest, mode)
 
 
-def unpack(work_env, mar, dest_dir):
+async def run_command(cmd, cwd='/', env=None, label=None, silent=False):
+    if not env:
+        env = dict()
+    process = await asyncio.create_subprocess_shell(cmd,
+                                                    stdout=asyncio.subprocess.PIPE,
+                                                    stderr=asyncio.subprocess.STDOUT,
+                                                    cwd=cwd, env=env)
+    stdout, stderr = await process.communicate()
+
+    await process.wait()
+
+    if silent:
+        return
+
+    if not stderr:
+        stderr = ""
+    if not stdout:
+        stdout = ""
+
+    label = "{}: ".format(label)
+
+    for line in stdout.splitlines():
+        log.debug("%s%s", label, line.decode('utf-8'))
+    for line in stderr.splitlines():
+        log.warn("%s%s", label, line.decode('utf-8'))
+
+
+async def unpack(work_env, mar, dest_dir):
     os.mkdir(dest_dir)
-    unwrap_cmd = sh.Command(os.path.join(work_env.workdir,
-                                         "unwrap_full_update.pl"))
     log.debug("Unwrapping %s", mar)
     env = work_env.env
     if not is_lzma_compressed_mar(mar):
         env['MAR_OLD_FORMAT'] = '1'
     elif 'MAR_OLD_FORMAT' in env:
         del env['MAR_OLD_FORMAT']
-    out = unwrap_cmd(mar, _cwd=dest_dir, _env=env, _timeout=240,
-                     _err_to_out=True)
-    if out:
-        log.debug(out)
+
+    cmd = "{} {}".format(work_env.paths['unwrap_full_update.pl'], mar)
+    await run_command(cmd, cwd=dest_dir, env=env, label=dest_dir)
 
 
 def find_file(directory, filename):
     log.debug("Searching for %s in %s", filename, directory)
-    for root, dirs, files in os.walk(directory):
+    for root, _, files in os.walk(directory):
         if filename in files:
             f = os.path.join(root, filename)
             log.debug("Found %s", f)
             return f
 
 
 def get_option(directory, filename, section, option):
-    log.debug("Exctracting [%s]: %s from %s/**/%s", section, option, directory,
+    log.debug("Extracting [%s]: %s from %s/**/%s", section, option, directory,
               filename)
     f = find_file(directory, filename)
     config = configparser.ConfigParser()
     config.read(f)
     rv = config.get(section, option)
     log.debug("Found %s", rv)
     return rv
 
 
-def generate_partial(work_env, from_dir, to_dir, dest_mar, channel_ids,
-                     version, use_old_format):
-    log.debug("Generating partial %s", dest_mar)
+async def generate_partial(work_env, from_dir, to_dir, dest_mar, channel_ids,
+                           version, use_old_format):
+    log.info("Generating partial %s", dest_mar)
     env = work_env.env
     env["MOZ_PRODUCT_VERSION"] = version
     env["MOZ_CHANNEL_ID"] = channel_ids
     if use_old_format:
         env['MAR_OLD_FORMAT'] = '1'
     elif 'MAR_OLD_FORMAT' in env:
         del env['MAR_OLD_FORMAT']
     make_incremental_update = os.path.join(work_env.workdir,
                                            "make_incremental_update.sh")
-    out = sh.bash(make_incremental_update, dest_mar, from_dir, to_dir,
-                  _cwd=work_env.workdir, _env=env, _timeout=900,
-                  _err_to_out=True)
-    if out:
-        log.debug(out)
+    cmd = " ".join([make_incremental_update, dest_mar, from_dir, to_dir])
+
+    await run_command(cmd, cwd=work_env.workdir, env=env, label=dest_mar.split('/')[-1])
 
 
 def get_hash(path, hash_type="sha512"):
     h = hashlib.new(hash_type)
     with open(path, "rb") as f:
         h.update(f.read())
     return h.hexdigest()
 
 
 class WorkEnv(object):
 
     def __init__(self):
         self.workdir = tempfile.mkdtemp()
+        self.paths = {
+            'unwrap_full_update.pl': os.path.join(self.workdir, 'unwrap_full_update.pl'),
+            'mar': os.path.join(self.workdir, 'mar'),
+            'mbsdiff': os.path.join(self.workdir, 'mbsdiff')
+        }
 
-    def setup(self):
-        self.download_unwrap()
-        self.download_martools()
+    async def setup(self):
+        await self.download_unwrap()
+        await self.download_martools()
 
-    def download_unwrap(self):
+    async def clone(self, workenv):
+        for path in workenv.paths:
+            if os.path.exists(self.paths[path]):
+                os.unlink(self.paths[path])
+            os.link(workenv.paths[path], self.paths[path])
+
+    async def download_unwrap(self):
         # unwrap_full_update.pl is not too sensitive to the revision
         url = "https://hg.mozilla.org/mozilla-central/raw-file/default/" \
             "tools/update-packaging/unwrap_full_update.pl"
-        download(url, dest=os.path.join(self.workdir, "unwrap_full_update.pl"),
-                 mode=0o755)
+        await retry_download(url, dest=self.paths['unwrap_full_update.pl'], mode=0o755)
 
-    def download_buildsystem_bits(self, repo, revision):
+    async def download_buildsystem_bits(self, repo, revision):
         prefix = "{repo}/raw-file/{revision}/tools/update-packaging"
         prefix = prefix.format(repo=repo, revision=revision)
-        for f in ("make_incremental_update.sh", "common.sh"):
+        for f in ('make_incremental_update.sh', 'common.sh'):
             url = "{prefix}/{f}".format(prefix=prefix, f=f)
-            download(url, dest=os.path.join(self.workdir, f), mode=0o755)
+            await retry_download(url, dest=os.path.join(self.workdir, f), mode=0o755)
 
-    def download_martools(self):
+    async def download_martools(self):
         # TODO: check if the tools have to be branch specific
         prefix = "https://ftp.mozilla.org/pub/mozilla.org/firefox/nightly/" \
             "latest-mozilla-central/mar-tools/linux64"
-        for f in ("mar", "mbsdiff"):
+        for f in ('mar', 'mbsdiff'):
             url = "{prefix}/{f}".format(prefix=prefix, f=f)
-            download(url, dest=os.path.join(self.workdir, f), mode=0o755)
+            await retry_download(url, dest=self.paths[f], mode=0o755)
 
     def cleanup(self):
         shutil.rmtree(self.workdir)
 
     @property
     def env(self):
         my_env = os.environ.copy()
         my_env['LC_ALL'] = 'C'
@@ -201,16 +257,161 @@ class WorkEnv(object):
 
 def verify_allowed_url(mar):
     if not any(mar.startswith(prefix) for prefix in ALLOWED_URL_PREFIXES):
         raise ValueError("{mar} is not in allowed URL prefixes: {p}".format(
             mar=mar, p=ALLOWED_URL_PREFIXES
         ))
 
 
+async def manage_partial(partial_def, work_env, filename_template, artifacts_dir, signing_certs):
+    """Manage the creation of partial mars based on payload."""
+    for mar in (partial_def["from_mar"], partial_def["to_mar"]):
+        verify_allowed_url(mar)
+
+    complete_mars = {}
+    use_old_format = False
+
+    for mar_type, f in (("from", partial_def["from_mar"]), ("to", partial_def["to_mar"])):
+        dest = os.path.join(work_env.workdir, "{}.mar".format(mar_type))
+        unpack_dir = os.path.join(work_env.workdir, mar_type)
+
+        with ddstats.timer('mar.download.time'):
+            await retry_download(f, dest)
+
+        if not os.getenv("MOZ_DISABLE_MAR_CERT_VERIFICATION"):
+            verify_signature(dest, signing_certs)
+
+        complete_mars["%s_size" % mar_type] = os.path.getsize(dest)
+        complete_mars["%s_hash" % mar_type] = get_hash(dest)
+
+        with ddstats.timer('mar.unpack.time'):
+            await unpack(work_env, dest, unpack_dir)
+
+        if mar_type == 'from':
+            version = get_option(unpack_dir, filename="application.ini",
+                                 section="App", option="Version")
+            major = int(version.split(".")[0])
+            # The updater for versions less than 56.0 requires BZ2
+            # compressed MAR files
+            if major < 56:
+                use_old_format = True
+                log.info("Forcing BZ2 compression for %s", f)
+
+        log.info("AV-scanning %s ...", unpack_dir)
+        metric_tags = [
+            "platform:{}".format(partial_def['platform']),
+        ]
+        with ddstats.timer('mar.clamscan.time', tags=metric_tags):
+            await run_command("clamscan -r {}".format(unpack_dir), label='clamscan')
+        log.info("Done.")
+
+    to_path = os.path.join(work_env.workdir, "to")
+    from_path = os.path.join(work_env.workdir, "from")
+
+    mar_data = {
+        "ACCEPTED_MAR_CHANNEL_IDS": get_option(
+            to_path, filename="update-settings.ini", section="Settings",
+            option="ACCEPTED_MAR_CHANNEL_IDS"),
+        "version": get_option(to_path, filename="application.ini",
+                              section="App", option="Version"),
+        "to_buildid": get_option(to_path, filename="application.ini",
+                                 section="App", option="BuildID"),
+        "from_buildid": get_option(from_path, filename="application.ini",
+                                   section="App", option="BuildID"),
+        "appName": get_option(from_path, filename="application.ini",
+                              section="App", option="Name"),
+        # Use Gecko repo and rev from platform.ini, not application.ini
+        "repo": get_option(to_path, filename="platform.ini", section="Build",
+                           option="SourceRepository"),
+        "revision": get_option(to_path, filename="platform.ini",
+                               section="Build", option="SourceStamp"),
+        "from_mar": partial_def["from_mar"],
+        "to_mar": partial_def["to_mar"],
+        "platform": partial_def["platform"],
+        "locale": partial_def["locale"],
+    }
+    # Override ACCEPTED_MAR_CHANNEL_IDS if needed
+    if "ACCEPTED_MAR_CHANNEL_IDS" in os.environ:
+        mar_data["ACCEPTED_MAR_CHANNEL_IDS"] = os.environ["ACCEPTED_MAR_CHANNEL_IDS"]
+    for field in ("update_number", "previousVersion", "previousBuildNumber",
+                  "toVersion", "toBuildNumber"):
+        if field in partial_def:
+            mar_data[field] = partial_def[field]
+    mar_data.update(complete_mars)
+
+    # if branch not set explicitly use repo-name
+    mar_data['branch'] = partial_def.get('branch', mar_data['repo'].rstrip('/').split('/')[-1])
+
+    if 'dest_mar' in partial_def:
+        mar_name = partial_def['dest_mar']
+    else:
+        # default to formatted name if not specified
+        mar_name = filename_template.format(**mar_data)
+
+    mar_data['mar'] = mar_name
+    dest_mar = os.path.join(work_env.workdir, mar_name)
+
+    # TODO: download these once
+    await work_env.download_buildsystem_bits(repo=mar_data["repo"],
+                                             revision=mar_data["revision"])
+
+    metric_tags = [
+        "branch:{}".format(mar_data['branch']),
+        "platform:{}".format(mar_data['platform']),
+        # If required. Shouldn't add much useful info, but increases
+        # cardinality of metrics substantially, so avoided.
+        # "locale:{}".format(mar_data['locale']),
+    ]
+    with ddstats.timer('generate_partial.time', tags=metric_tags):
+        await generate_partial(work_env, from_path, to_path, dest_mar,
+                               mar_data["ACCEPTED_MAR_CHANNEL_IDS"],
+                               mar_data["version"],
+                               use_old_format)
+
+    mar_data["size"] = os.path.getsize(dest_mar)
+
+    metric_tags.append("unit:bytes")
+    # Allows us to find out how many releases there were between the two,
+    # making buckets of the file sizes easier.
+    metric_tags.append("update_number:{}".format(mar_data.get('update_number', 0)))
+    ddstats.gauge('partial_mar_size', mar_data['size'], tags=metric_tags)
+
+    mar_data["hash"] = get_hash(dest_mar)
+
+    shutil.copy(dest_mar, artifacts_dir)
+    work_env.cleanup()
+
+    return mar_data
+
+
+async def async_main(args, signing_certs):
+    tasks = []
+
+    master_env = WorkEnv()
+    await master_env.setup()
+
+    task = json.load(args.task_definition)
+    # TODO: verify task["extra"]["funsize"]["partials"] with jsonschema
+    for definition in task["extra"]["funsize"]["partials"]:
+        workenv = WorkEnv()
+        await workenv.clone(master_env)
+        tasks.append(asyncio.ensure_future(manage_partial(
+            partial_def=definition,
+            filename_template=args.filename_template,
+            artifacts_dir=args.artifacts_dir,
+            work_env=workenv,
+            signing_certs=signing_certs)
+        ))
+
+    manifest = await asyncio.gather(*tasks)
+    master_env.cleanup()
+    return manifest
+
+
 def main():
 
     start = time.time()
 
     parser = argparse.ArgumentParser()
     parser.add_argument("--artifacts-dir", required=True)
     parser.add_argument("--sha1-signing-cert", required=True)
     parser.add_argument("--sha384-signing-cert", required=True)
@@ -222,37 +423,31 @@ def main():
                         help="Do not refresh ClamAV DB")
     parser.add_argument("-q", "--quiet", dest="log_level",
                         action="store_const", const=logging.WARNING,
                         default=logging.DEBUG)
     args = parser.parse_args()
 
     logging.basicConfig(format="%(asctime)s - %(levelname)s - %(message)s")
     log.setLevel(args.log_level)
-    task = json.load(args.task_definition)
-    # TODO: verify task["extra"]["funsize"]["partials"] with jsonschema
 
     signing_certs = {
         'sha1': open(args.sha1_signing_cert, 'rb').read(),
         'sha384': open(args.sha384_signing_cert, 'rb').read(),
     }
 
     assert(get_keysize(signing_certs['sha1']) == 2048)
     assert(get_keysize(signing_certs['sha384']) == 4096)
 
     # Intended for local testing.
     dd_api_key = os.environ.get('DATADOG_API_KEY')
     # Intended for Taskcluster.
     if not dd_api_key and os.environ.get('DATADOG_API_SECRET'):
         dd_api_key = get_secret(os.environ.get('DATADOG_API_SECRET')).get('key')
 
-    # Create this even when not sending metrics, so the context manager
-    # statements work.
-    ddstats = ThreadStats(namespace='releng.releases.partials')
-
     if dd_api_key:
         dd_options = {
             'api_key': dd_api_key,
         }
         log.info("Starting metric collection")
         initialize(**dd_options)
         ddstats.start(flush_interval=1)
     else:
@@ -264,139 +459,35 @@ def main():
         log.info("Refreshing clamav db...")
         try:
             redo.retry(lambda: sh.freshclam("--stdout", "--verbose",
                                             _timeout=300, _err_to_out=True))
             log.info("Done.")
         except sh.ErrorReturnCode:
             log.warning("Freshclam failed, skipping DB update")
 
-    manifest = []
-    for e in task["extra"]["funsize"]["partials"]:
-        for mar in (e["from_mar"], e["to_mar"]):
-            verify_allowed_url(mar)
-
-        work_env = WorkEnv()
-        # TODO: run setup once
-        work_env.setup()
-        complete_mars = {}
-        use_old_format = False
-        for mar_type, f in (("from", e["from_mar"]), ("to", e["to_mar"])):
-            dest = os.path.join(work_env.workdir, "{}.mar".format(mar_type))
-            unpack_dir = os.path.join(work_env.workdir, mar_type)
-            with ddstats.timer('mar.download.time'):
-                download(f, dest)
-            if not os.getenv("MOZ_DISABLE_MAR_CERT_VERIFICATION"):
-                verify_signature(dest, signing_certs)
-            complete_mars["%s_size" % mar_type] = os.path.getsize(dest)
-            complete_mars["%s_hash" % mar_type] = get_hash(dest)
-            with ddstats.timer('mar.unpack.time'):
-                unpack(work_env, dest, unpack_dir)
-            if mar_type == 'from':
-                version = get_option(unpack_dir, filename="application.ini",
-                                     section="App", option="Version")
-                major = int(version.split(".")[0])
-                # The updater for versions less than 56.0 requires BZ2
-                # compressed MAR files
-                if major < 56:
-                    use_old_format = True
-                    log.info("Forcing BZ2 compression for %s", f)
-            log.info("AV-scanning %s ...", unpack_dir)
-            metric_tags = [
-                "platform:{}".format(e['platform']),
-            ]
-            with ddstats.timer('mar.clamscan.time', tags=metric_tags):
-                sh.clamscan("-r", unpack_dir, _timeout=600, _err_to_out=True)
-            log.info("Done.")
-
-        path = os.path.join(work_env.workdir, "to")
-        from_path = os.path.join(work_env.workdir, "from")
-        mar_data = {
-            "ACCEPTED_MAR_CHANNEL_IDS": get_option(
-                path, filename="update-settings.ini", section="Settings",
-                option="ACCEPTED_MAR_CHANNEL_IDS"),
-            "version": get_option(path, filename="application.ini",
-                                  section="App", option="Version"),
-            "to_buildid": get_option(path, filename="application.ini",
-                                     section="App", option="BuildID"),
-            "from_buildid": get_option(from_path, filename="application.ini",
-                                       section="App", option="BuildID"),
-            "appName": get_option(from_path, filename="application.ini",
-                                  section="App", option="Name"),
-            # Use Gecko repo and rev from platform.ini, not application.ini
-            "repo": get_option(path, filename="platform.ini", section="Build",
-                               option="SourceRepository"),
-            "revision": get_option(path, filename="platform.ini",
-                                   section="Build", option="SourceStamp"),
-            "from_mar": e["from_mar"],
-            "to_mar": e["to_mar"],
-            "platform": e["platform"],
-            "locale": e["locale"],
-        }
-        # Override ACCEPTED_MAR_CHANNEL_IDS if needed
-        if "ACCEPTED_MAR_CHANNEL_IDS" in os.environ:
-            mar_data["ACCEPTED_MAR_CHANNEL_IDS"] = os.environ["ACCEPTED_MAR_CHANNEL_IDS"]
-        for field in ("update_number", "previousVersion",
-                      "previousBuildNumber", "toVersion",
-                      "toBuildNumber"):
-            if field in e:
-                mar_data[field] = e[field]
-        mar_data.update(complete_mars)
-        # if branch not set explicitly use repo-name
-        mar_data["branch"] = e.get("branch",
-                                   mar_data["repo"].rstrip("/").split("/")[-1])
-        if 'dest_mar' in e:
-            mar_name = e['dest_mar']
-        else:
-            # default to formatted name if not specified
-            mar_name = args.filename_template.format(**mar_data)
-        mar_data["mar"] = mar_name
-        dest_mar = os.path.join(work_env.workdir, mar_name)
-        # TODO: download these once
-        work_env.download_buildsystem_bits(repo=mar_data["repo"],
-                                           revision=mar_data["revision"])
-
-        metric_tags = [
-            "branch:{}".format(mar_data['branch']),
-            "platform:{}".format(mar_data['platform']),
-            # If required. Shouldn't add much useful info, but increases
-            # cardinality of metrics substantially, so avoided.
-            # "locale:{}".format(mar_data['locale']),
-        ]
-
-        with ddstats.timer('generate_partial.time', tags=metric_tags):
-            generate_partial(work_env, from_path, path, dest_mar,
-                             mar_data["ACCEPTED_MAR_CHANNEL_IDS"],
-                             mar_data["version"],
-                             use_old_format)
-
-        mar_data["size"] = os.path.getsize(dest_mar)
-        metric_tags.append("unit:bytes")
-        # Allows us to find out how many releases there were between the two,
-        # making buckets of the file sizes easier.
-        metric_tags.append("update_number:{}".format(mar_data.get('update_number', 0)))
-        ddstats.gauge('partial_mar_size', mar_data['size'], tags=metric_tags)
-
-        mar_data["hash"] = get_hash(dest_mar)
-
-        shutil.copy(dest_mar, args.artifacts_dir)
-        work_env.cleanup()
-        manifest.append(mar_data)
+    loop = asyncio.get_event_loop()
+    manifest = loop.run_until_complete(async_main(args, signing_certs))
+    loop.close()
 
     manifest_file = os.path.join(args.artifacts_dir, "manifest.json")
     with open(manifest_file, "w") as fp:
         json.dump(manifest, fp, indent=2, sort_keys=True)
 
+    log.debug("{}".format(json.dumps(manifest, indent=2, sort_keys=True)))
+
     # Warning: Assumption that one partials task will always be for one branch.
     metric_tags = [
-        "branch:{}".format(mar_data['branch']),
+        "branch:{}".format(manifest[0]['branch']),
     ]
 
     ddstats.timing('task_duration', time.time() - start,
                    start, tags=metric_tags)
+
     # Wait for all the metrics to flush. If the program ends before
     # they've been sent, they'll be dropped.
     # Should be more than the flush_interval for the ThreadStats object
-    time.sleep(10)
+    if dd_api_key:
+        time.sleep(10)
 
 
 if __name__ == '__main__':
     main()
new file mode 100644
--- /dev/null
+++ b/taskcluster/docker/google-play-strings/Dockerfile
@@ -0,0 +1,19 @@
+FROM          ubuntu:16.04
+MAINTAINER    Johan Lorenzo <jlorenzo+tc@mozilla.com>
+
+RUN mkdir /builds
+RUN groupadd -g 500 worker
+RUN useradd -u 500 -g 500 -d /builds/worker -s /bin/bash -m worker
+
+RUN apt-get update
+RUN apt-get install --yes git python3-setuptools build-essential libssl-dev libffi-dev python3-dev
+
+WORKDIR /builds/worker/
+RUN git clone https://github.com/mozilla-releng/mozapkpublisher
+WORKDIR /builds/worker/mozapkpublisher
+RUN python3 setup.py develop
+
+RUN chown -R worker:worker /builds/worker
+
+# Set a default command useful for debugging
+CMD ["/bin/bash", "--login"]
--- a/taskcluster/docs/kinds.rst
+++ b/taskcluster/docs/kinds.rst
@@ -212,16 +212,21 @@ and sign it via the signing scriptworker
 additional detached signature.
 
 beetmover-checksums
 -------------------
 Beetmover, takes specific artifact checksums and pushes it to a location outside
 of Taskcluster's task artifacts (archive.mozilla.org as one place) and in the
 process determines the final location and "pretty" names it (version product name)
 
+google-play-strings
+-------------------
+Download strings to display on Google Play from https://l10n.mozilla-community.org/stores_l10n/.
+Artifact is then used by push-apk.
+
 push-apk-breakpoint
 -------------------
 Decides whether or not APKs should be published onto Google Play Store. Jobs of this
 kind depend on all the signed multi-locales (aka "multi") APKs for a given release,
 in order to make the decision.
 
 push-apk
 --------
new file mode 100644
--- /dev/null
+++ b/taskcluster/taskgraph/transforms/google_play_strings.py
@@ -0,0 +1,64 @@
+# 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/.
+"""
+Transform the push-apk kind into an actual task description.
+"""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+import functools
+
+from taskgraph.transforms.base import TransformSequence
+from taskgraph.transforms.task import task_description_schema
+from taskgraph.util.schema import resolve_keyed_by, Schema
+from taskgraph.util.push_apk import fill_labels_tranform, validate_jobs_schema_transform_partial
+
+from voluptuous import Required
+
+
+transforms = TransformSequence()
+
+# Voluptuous uses marker objects as dictionary *keys*, but they are not
+# comparable, so we cast all of the keys back to regular strings
+task_description_schema = {str(k): v for k, v in task_description_schema.schema.iteritems()}
+
+google_play_description_schema = Schema({
+    Required('name'): basestring,
+    Required('label'): task_description_schema['label'],
+    Required('description'): task_description_schema['description'],
+    Required('job-from'): task_description_schema['job-from'],
+    Required('attributes'): task_description_schema['attributes'],
+    Required('treeherder'): task_description_schema['treeherder'],
+    Required('run-on-projects'): task_description_schema['run-on-projects'],
+    Required('shipping-phase'): task_description_schema['shipping-phase'],
+    Required('shipping-product'): task_description_schema['shipping-product'],
+    Required('worker-type'): task_description_schema['worker-type'],
+    Required('worker'): object,
+})
+
+validate_jobs_schema_transform = functools.partial(
+    validate_jobs_schema_transform_partial,
+    google_play_description_schema,
+    'GooglePlayStrings'
+)
+
+transforms.add(fill_labels_tranform)
+transforms.add(validate_jobs_schema_transform)
+
+
+@transforms.add
+def set_worker_data(config, jobs):
+    for job in jobs:
+        worker = job['worker']
+
+        env = worker.setdefault('env', {})
+        resolve_keyed_by(
+            env, 'PACKAGE_NAME', item_name=job['name'],
+            project=config.params['project']
+        )
+
+        cot = job.setdefault('extra', {}).setdefault('chainOfTrust', {})
+        cot.setdefault('inputs', {})['docker-image'] = {'task-reference': '<docker-image>'}
+
+        yield job
--- a/taskcluster/taskgraph/transforms/push_apk.py
+++ b/taskcluster/taskgraph/transforms/push_apk.py
@@ -23,32 +23,32 @@ from voluptuous import Optional, Require
 transforms = TransformSequence()
 
 # Voluptuous uses marker objects as dictionary *keys*, but they are not
 # comparable, so we cast all of the keys back to regular strings
 task_description_schema = {str(k): v for k, v in task_description_schema.schema.iteritems()}
 
 
 push_apk_description_schema = Schema({
-    # the dependent task (object) for this beetmover job, used to inform beetmover.
     Required('dependent-tasks'): object,
     Required('name'): basestring,
-    Required('label'): basestring,
-    Required('description'): basestring,
-    Required('job-from'): basestring,
-    Required('attributes'): object,
-    Required('treeherder'): object,
-    Required('run-on-projects'): list,
+    Required('label'): task_description_schema['label'],
+    Required('description'): task_description_schema['description'],
+    Required('job-from'): task_description_schema['job-from'],
+    Required('attributes'): task_description_schema['attributes'],
+    Required('treeherder'): task_description_schema['treeherder'],
+    Required('run-on-projects'): task_description_schema['run-on-projects'],
     Required('worker-type'): optionally_keyed_by('project', basestring),
     Required('worker'): object,
     Required('scopes'): None,
+    Required('requires'): task_description_schema['requires'],
     Required('deadline-after'): basestring,
     Required('shipping-phase'): task_description_schema['shipping-phase'],
     Required('shipping-product'): task_description_schema['shipping-product'],
-    Optional('extra'): object,
+    Optional('extra'): task_description_schema['extra'],
 })
 
 validate_jobs_schema_transform = functools.partial(
     validate_jobs_schema_transform_partial,
     push_apk_description_schema,
     'PushApk'
 )
 
@@ -78,13 +78,26 @@ def make_task_description(config, jobs):
 
         yield job
 
 
 transforms.add(delete_non_required_fields_transform)
 
 
 def generate_upstream_artifacts(dependencies):
-    return [{
+    apks = [{
         'taskId': {'task-reference': '<{}>'.format(task_kind)},
         'taskType': 'signing',
         'paths': ['public/build/target.apk'],
-    } for task_kind in dependencies.keys() if 'breakpoint' not in task_kind]
+    } for task_kind in dependencies.keys()
+      if task_kind not in ('push-apk-breakpoint', 'google-play-strings')
+    ]
+
+    google_play_strings = [{
+        'taskId': {'task-reference': '<{}>'.format(task_kind)},
+        'taskType': 'build',
+        'paths': ['public/google_play_strings.json'],
+        'optional': True,
+    } for task_kind in dependencies.keys()
+      if 'google-play-strings' in task_kind
+    ]
+
+    return apks + google_play_strings
--- a/taskcluster/taskgraph/transforms/task.py
+++ b/taskcluster/taskgraph/transforms/task.py
@@ -88,16 +88,18 @@ task_description_schema = Schema({
     # relative path (from config.path) to the file task was defined in
     Optional('job-from'): basestring,
 
     # dependencies of this task, keyed by name; these are passed through
     # verbatim and subject to the interpretation of the Task's get_dependencies
     # method.
     Optional('dependencies'): {basestring: object},
 
+    Optional('requires'): Any('all-completed', 'all-resolved'),
+
     # expiration and deadline times, relative to task creation, with units
     # (e.g., "14 days").  Defaults are set based on the project.
     Optional('expires-after'): basestring,
     Optional('deadline-after'): basestring,
 
     # custom routes for this task; the default treeherder routes will be added
     # automatically
     Optional('routes'): [basestring],
@@ -566,16 +568,19 @@ task_description_schema = Schema({
             # taskId of the task with the artifact
             Required('taskId'): taskref_or_string,
 
             # type of signing task (for CoT)
             Required('taskType'): basestring,
 
             # Paths to the artifacts to sign
             Required('paths'): [basestring],
+
+            # Artifact is optional to run the task
+            Optional('optional', default=False): bool,
         }],
 
         # "Invalid" is a noop for try and other non-supported branches
         Required('google-play-track'): Any('production', 'beta', 'alpha', 'rollout', 'invalid'),
         Required('commit'): bool,
         Optional('rollout-percentage'): int,
     }),
 })
@@ -1466,16 +1471,19 @@ def build_task(config, tasks):
                     config.params['head_rev'],
                     config.path),
             },
             'extra': extra,
             'tags': tags,
             'priority': task['priority'],
         }
 
+        if task.get('requires', None):
+            task_def['requires'] = task['requires']
+
         if task_th:
             # link back to treeherder in description
             th_push_link = 'https://treeherder.mozilla.org/#/jobs?repo={}&revision={}'.format(
                 config.params['project'], treeherder_rev)
             task_def['metadata']['description'] += ' ([Treeherder push]({}))'.format(
                 th_push_link)
 
         # add the payload and adjust anything else as required (e.g., scopes)
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -273,28 +273,30 @@ Object.defineProperty(GeckoDriver.protot
 
 GeckoDriver.prototype.QueryInterface = XPCOMUtils.generateQI([
   Ci.nsIMessageListener,
   Ci.nsIObserver,
   Ci.nsISupportsWeakReference,
 ]);
 
 GeckoDriver.prototype.init = function() {
+  this.mm.addMessageListener("Marionette:WebDriver:GetCapabilities", this);
   this.mm.addMessageListener("Marionette:GetLogLevel", this);
   this.mm.addMessageListener("Marionette:getVisibleCookies", this);
-  this.mm.addMessageListener("Marionette:listenersAttached", this);
-  this.mm.addMessageListener("Marionette:register", this);
+  this.mm.addMessageListener("Marionette:ListenersAttached", this);
+  this.mm.addMessageListener("Marionette:Register", this);
   this.mm.addMessageListener("Marionette:switchedToFrame", this);
 };
 
 GeckoDriver.prototype.uninit = function() {
+  this.mm.removeMessageListener("Marionette:WebDriver:GetCapabilities", this);
   this.mm.removeMessageListener("Marionette:GetLogLevel", this);
   this.mm.removeMessageListener("Marionette:getVisibleCookies", this);
-  this.mm.removeMessageListener("Marionette:listenersAttached", this);
-  this.mm.removeMessageListener("Marionette:register", this);
+  this.mm.removeMessageListener("Marionette:ListenersAttached", this);
+  this.mm.removeMessageListener("Marionette:Register", this);
   this.mm.removeMessageListener("Marionette:switchedToFrame", this);
 };
 
 /**
  * Callback used to observe the creation of new modal or tab modal dialogs
  * during the session's lifetime.
  */
 GeckoDriver.prototype.globalModalDialogHandler = function(subject, topic) {
@@ -438,17 +440,16 @@ GeckoDriver.prototype.addBrowser = funct
  *     Window whose browser we need to access.
  * @param {boolean=} [false] isNewSession
  *     True if this is the first time we're talking to this browser.
  */
 GeckoDriver.prototype.startBrowser = function(window, isNewSession = false) {
   this.mainFrame = window;
   this.curFrame = null;
   this.addBrowser(window);
-  this.curBrowser.isNewSession = isNewSession;
   this.whenBrowserStarted(window, isNewSession);
 };
 
 /**
  * Callback invoked after a new session has been started in a browser.
  * Loads the Marionette frame script into the browser if needed.
  *
  * @param {ChromeWindow} window
@@ -531,59 +532,53 @@ GeckoDriver.prototype.registerBrowser = 
   if (this.appId != APP_ID_FIREFOX || be.namespaceURI != XUL_NS ||
       be.nodeName != "browser" || be.getTabBrowser()) {
     // curBrowser holds all the registered frames in knownFrames
     this.curBrowser.register(id, be);
   }
 
   this.wins.set(id, listenerWindow);
   if (nullPrevious && (this.curBrowser.curFrameId !== null)) {
-    this.sendAsync(
-        "newSession",
-        this.capabilities,
-        this.newSessionCommandId);
-    if (this.curBrowser.isNewSession) {
-      this.newSessionCommandId = null;
-    }
+    this.sendAsync("newSession");
   }
 
-  return [id, this.capabilities.toJSON()];
+  return id;
 };
 
 GeckoDriver.prototype.registerPromise = function() {
-  const li = "Marionette:register";
+  const li = "Marionette:Register";
 
   return new Promise(resolve => {
     let cb = msg => {
       let wid = msg.json.value;
       let be = msg.target;
-      let rv = this.registerBrowser(wid, be);
+      let outerWindowID = this.registerBrowser(wid, be);
 
       if (this.curBrowser.frameRegsPending > 0) {
         this.curBrowser.frameRegsPending--;
       }
 
       if (this.curBrowser.frameRegsPending === 0) {
         this.mm.removeMessageListener(li, cb);
         resolve();
       }
 
       // this is a sync message and listeners expect the ID back
-      return rv;
+      return outerWindowID;
     };
     this.mm.addMessageListener(li, cb);
   });
 };
 
 GeckoDriver.prototype.listeningPromise = function() {
-  const li = "Marionette:listenersAttached";
+  const li = "Marionette:ListenersAttached";
 
   return new Promise(resolve => {
     let cb = msg => {
-      if (msg.json.listenerId === this.curBrowser.curFrameId) {
+      if (msg.json.outerWindowID === this.curBrowser.curFrameId) {
         this.mm.removeMessageListener(li, cb);
         resolve();
       }
     };
     this.mm.addMessageListener(li, cb);
   });
 };
 
@@ -694,17 +689,16 @@ GeckoDriver.prototype.listeningPromise =
  * @throws {SessionNotCreatedError}
  *     If, for whatever reason, a session could not be created.
  */
 GeckoDriver.prototype.newSession = async function(cmd) {
   if (this.sessionID) {
     throw new SessionNotCreatedError("Maximum number of active sessions");
   }
   this.sessionID = WebElement.generateUUID();
-  this.newSessionCommandId = cmd.id;
 
   try {
     this.capabilities = session.Capabilities.fromJSON(cmd.parameters);
 
     if (!this.secureTLS) {
       logger.warn("TLS certificate errors will be ignored for this session");
       let acceptAllCerts = new cert.InsecureSweepingOverride();
       cert.installOverride(acceptAllCerts);
@@ -3338,32 +3332,35 @@ GeckoDriver.prototype.receiveMessage = f
           this.currentFrameElement =
               new ChromeWebElement(message.json.frameValue);
         } else {
           this.currentFrameElement = null;
         }
       }
       break;
 
-    case "Marionette:register":
+    case "Marionette:Register":
       let wid = message.json.value;
       let be = message.target;
-      let rv = this.registerBrowser(wid, be);
-      return rv;
-
-    case "Marionette:listenersAttached":
-      if (message.json.listenerId === this.curBrowser.curFrameId) {
+      let outerWindowID = this.registerBrowser(wid, be);
+      return {outerWindowID};
+
+    case "Marionette:ListenersAttached":
+      if (message.json.outerWindowID === this.curBrowser.curFrameId) {
         // If the frame script gets reloaded we need to call newSession.
         // In the case of desktop this just sets up a small amount of state
         // that doesn't change over the course of a session.
-        this.sendAsync("newSession", this.capabilities);
+        this.sendAsync("newSession");
         this.curBrowser.flushPendingCommands();
       }
       break;
 
+    case "Marionette:WebDriver:GetCapabilities":
+      return this.capabilities.toJSON();
+
     case "Marionette:GetLogLevel":
       return logger.level;
   }
 };
 /* eslint-enable consistent-return */
 
 GeckoDriver.prototype.responseCompleted = function() {
   if (this.curBrowser !== null) {
--- a/testing/marionette/listener.js
+++ b/testing/marionette/listener.js
@@ -42,17 +42,17 @@ const {ContentEventObserverService} = Cu
 Cu.import("chrome://marionette/content/interaction.js");
 Cu.import("chrome://marionette/content/legacyaction.js");
 Cu.import("chrome://marionette/content/navigate.js");
 Cu.import("chrome://marionette/content/proxy.js");
 Cu.import("chrome://marionette/content/session.js");
 
 Cu.importGlobalProperties(["URL"]);
 
-let listenerId = null; // unique ID of this listener
+let outerWindowID = null;
 let curContainer = {frame: content, shadowRoot: null};
 
 // Listen for click event to indicate one click has happened, so actions
 // code can send dblclick event, also resetClick and cancelTimer
 // after dblclick has happened.
 addEventListener("click", event.DoubleClickTracker.setClick);
 addEventListener("dblclick", event.DoubleClickTracker.resetClick);
 addEventListener("dblclick", event.DoubleClickTracker.cancelTimer);
@@ -64,17 +64,23 @@ const SUPPORTED_STRATEGIES = new Set([
   element.Strategy.ID,
   element.Strategy.Name,
   element.Strategy.LinkText,
   element.Strategy.PartialLinkText,
   element.Strategy.TagName,
   element.Strategy.XPath,
 ]);
 
-let capabilities;
+Object.defineProperty(this, "capabilities", {
+  get() {
+    let payload = sendSyncMessage("Marionette:WebDriver:GetCapabilities");
+    return session.Capabilities.fromJSON(payload[0]);
+  },
+  configurable: true,
+});
 
 let legacyactions = new legacyaction.Chain();
 
 // last touch for each fingerId
 let multiLast = {};
 
 // TODO: Log.jsm is not e10s compatible (see https://bugzil.la/1411513),
 // query the main process for the current log level
@@ -442,25 +448,21 @@ const loadListener = {
  * an ID, we start the listeners. Otherwise, nothing happens.
  */
 function registerSelf() {
   let msg = {value: winUtil.outerWindowID};
   logger.debug(`Register listener.js for window ${msg.value}`);
 
   // register will have the ID and a boolean describing if this is the
   // main process or not
-  let register = sendSyncMessage("Marionette:register", msg);
+  let register = sendSyncMessage("Marionette:Register", msg);
   if (register[0]) {
-    listenerId = register[0][0];
-    capabilities = session.Capabilities.fromJSON(register[0][1]);
-    if (typeof listenerId != "undefined") {
-      startListeners();
-      sendAsyncMessage("Marionette:listenersAttached",
-          {"listenerId": listenerId});
-    }
+    outerWindowID = register[0].outerWindowID;
+    startListeners();
+    sendAsyncMessage("Marionette:ListenersAttached", {outerWindowID});
   }
 }
 
 // Eventually we will not have a closure for every single command,
 // but use a generic dispatch for all listener commands.
 //
 // Worth nothing that this shares many characteristics with
 // server.TCPConnection#execute.  Perhaps this could be generalised
@@ -485,28 +487,22 @@ function dispatch(fn) {
       resolve(rv);
     });
 
     req.then(rv => sendResponse(rv, id), err => sendError(err, id))
         .catch(err => sendError(err, id));
   };
 }
 
-/**
- * Add a message listener that's tied to our listenerId.
- */
 function addMessageListenerId(messageName, handler) {
-  addMessageListener(messageName + listenerId, handler);
+  addMessageListener(messageName + outerWindowID, handler);
 }
 
-/**
- * Remove a message listener that's tied to our listenerId.
- */
 function removeMessageListenerId(messageName, handler) {
-  removeMessageListener(messageName + listenerId, handler);
+  removeMessageListener(messageName + outerWindowID, handler);
 }
 
 let getPageSourceFn = dispatch(getPageSource);
 let getActiveElementFn = dispatch(getActiveElement);
 let getElementAttributeFn = dispatch(getElementAttribute);
 let getElementPropertyFn = dispatch(getElementProperty);
 let getElementTextFn = dispatch(getElementText);
 let getElementTagNameFn = dispatch(getElementTagName);
@@ -577,19 +573,22 @@ function startListeners() {
   addMessageListener("Marionette:DOM:AddEventListener", domAddEventListener);
   addMessageListener("Marionette:DOM:RemoveEventListener", domRemoveEventListener);
 }
 
 /**
  * Called when we start a new session. It registers the
  * current environment, and resets all values
  */
-function newSession(msg) {
-  capabilities = session.Capabilities.fromJSON(msg.json);
-  resetValues();
+function newSession() {
+  sandboxes.clear();
+  curContainer = {frame: content, shadowRoot: null};
+  legacyactions.mouseEventsOnly = false;
+  action.inputStateMap = new Map();
+  action.inputsToCancel = [];
 }
 
 /**
  * Removes all listeners
  */
 function deleteSession() {
   removeMessageListenerId("Marionette:newSession", newSession);
   removeMessageListenerId("Marionette:execute", executeFn);
@@ -701,27 +700,16 @@ function sendOk(uuid) {
  *     Error to notify chrome of.
  * @param {UUID} uuid
  *     Unique identifier of the request.
  */
 function sendError(err, uuid) {
   sendToServer(uuid, err);
 }
 
-/**
- * Clear test values after completion of test
- */
-function resetValues() {
-  sandboxes.clear();
-  curContainer = {frame: content, shadowRoot: null};
-  legacyactions.mouseEventsOnly = false;
-  action.inputStateMap = new Map();
-  action.inputsToCancel = [];
-}
-
 async function execute(script, args, timeout, opts) {
   opts.timeout = timeout;
   let sb = sandbox.createMutable(curContainer.frame);
   return evaluate.sandbox(sb, script, args, opts);
 }
 
 async function executeInSandbox(script, args, timeout, opts) {
   opts.timeout = timeout;
--- a/testing/web-platform/meta/custom-elements/reactions/CSSStyleDeclaration.html.ini
+++ b/testing/web-platform/meta/custom-elements/reactions/CSSStyleDeclaration.html.ini
@@ -7,19 +7,16 @@
     expected: FAIL
 
   [setProperty on CSSStyleDeclaration must enqueue an attributeChanged reaction when it adds the observed style attribute]
     expected: FAIL
 
   [setProperty on CSSStyleDeclaration must enqueue an attributeChanged reaction when it mutates the observed style attribute]
     expected: FAIL
 
-  [setProperty on CSSStyleDeclaration must enqueue an attributeChanged reaction when it makes a property important and the style attribute is observed]
-    expected: FAIL
-
   [setPropertyValue on CSSStyleDeclaration must enqueue an attributeChanged reaction when it adds the observed style attribute]
     expected: FAIL
 
   [setPropertyValue on CSSStyleDeclaration must not enqueue an attributeChanged reaction when it adds the style attribute but the style attribute is not observed]
     expected: FAIL
 
   [setPropertyValue on CSSStyleDeclaration must enqueue an attributeChanged reaction when it mutates the observed style attribute]
     expected: FAIL
@@ -28,19 +25,16 @@
     expected: FAIL
 
   [setPropertyPriority on CSSStyleDeclaration must enqueue an attributeChanged reaction when it makes a property important and the style attribute is observed]
     expected: FAIL
 
   [setPropertyPriority on CSSStyleDeclaration must enqueue an attributeChanged reaction when it makes a property important but the style attribute is not observed]
     expected: FAIL
 
-  [removeProperty on CSSStyleDeclaration must enqueue an attributeChanged reaction when it removes a property from the observed style attribute]
-    expected: FAIL
-
   [cssFloat on CSSStyleDeclaration must enqueue an attributeChanged reaction when it adds the observed style attribute]
     expected: FAIL
 
   [A camel case attribute (borderWidth) on CSSStyleDeclaration must enqueue an attributeChanged reaction when it adds the observed style attribute]
     expected: FAIL
 
   [A camel case attribute (borderWidth) on CSSStyleDeclaration must enqueue an attributeChanged reaction when it mutates the observed style attribute]
     expected: FAIL
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -5239,24 +5239,28 @@
     "n_buckets" : 100,
     "description": "PLACES: Size of favicon files without a specific file type probe, loaded from the web (Bytes)"
   },
   "LINK_ICON_SIZES_ATTR_USAGE": {
     "record_in_processes": ["main", "content"],
     "expires_in_version" : "never",
     "kind": "enumerated",
     "n_values": 4,
+    "alert_emails": ["fx-search@mozilla.com"],
+    "bug_numbers": [1053467],
     "description": "The possible types of the 'sizes' attribute for <link rel=icon>. 0: Attribute not specified, 1: 'any', 2: Integer dimensions, 3: Invalid value."
   },
   "LINK_ICON_SIZES_ATTR_DIMENSION": {
     "record_in_processes": ["main", "content"],
     "expires_in_version" : "never",
     "kind": "linear",
     "high": 513,
     "n_buckets" : 64,
+    "alert_emails": ["fx-search@mozilla.com"],
+    "bug_numbers": [1053467],
     "description": "The width dimension of the 'sizes' attribute for <link rel=icon>."
   },
   "PAGE_METADATA_SIZE": {
     "record_in_processes": ["main", "content"],
     "expires_in_version": "never",
     "kind": "linear",
     "high": 500,
     "n_buckets": 10,
@@ -7807,52 +7811,31 @@
     "description": "New tab page is enabled."
   },
   "NEWTAB_PAGE_ENHANCED": {
     "record_in_processes": ["main", "content"],
     "expires_in_version": "default",
     "kind": "boolean",
     "description": "New tab page is enhanced (showing suggestions)."
   },
-  "NEWTAB_PAGE_LIFE_SPAN": {
-    "record_in_processes": ["main", "content"],
-    "expires_in_version": "default",
-    "kind": "exponential",
-    "high": 1200,
-    "n_buckets": 100,
-    "description": "Life-span of a new tab without suggested tile: time delta between first-visible and unload events (half-seconds)."
-  },
   "NEWTAB_PAGE_PINNED_SITES_COUNT": {
     "record_in_processes": ["main", "content"],
     "expires_in_version": "default",
     "kind": "enumerated",
     "n_values": 9,
     "description": "Number of pinned sites on the new tab page."
   },
   "NEWTAB_PAGE_BLOCKED_SITES_COUNT": {
     "record_in_processes": ["main", "content"],
     "expires_in_version": "default",
     "kind": "exponential",
     "high": 100,
     "n_buckets": 10,
     "description": "Number of sites blocked from the new tab page."
   },
-  "NEWTAB_PAGE_SHOWN": {
-    "record_in_processes": ["main", "content"],
-    "expires_in_version": "35",
-    "kind": "boolean",
-    "description": "Number of times about:newtab was shown from opening a new tab or window. *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
-  },
-  "NEWTAB_PAGE_SITE_CLICKED": {
-    "record_in_processes": ["main", "content"],
-    "expires_in_version": "35",
-    "kind": "enumerated",
-    "n_values": 10,
-    "description": "Track click count on about:newtab tiles per index (0-8). For non-default row or column configurations all clicks into the '9' bucket. *** No longer needed (bug 1156565). Delete histogram and accumulation code! ***"
-  },
   "BROWSERPROVIDER_XUL_IMPORT_BOOKMARKS": {
     "record_in_processes": ["main", "content"],
     "expires_in_version": "default",
     "kind": "exponential",
     "high": 50000,
     "n_buckets": 20,
     "description": "Number of bookmarks in the original XUL places database",
     "cpp_guard": "ANDROID"
--- a/toolkit/components/telemetry/histogram-whitelists.json
+++ b/toolkit/components/telemetry/histogram-whitelists.json
@@ -283,18 +283,16 @@
     "HTTP_SUB_OPEN_TO_FIRST_SENT",
     "HTTP_SUB_REVALIDATION",
     "HTTP_TRANSACTION_USE_ALTSVC",
     "HTTP_TRANSACTION_USE_ALTSVC_OE",
     "INNERWINDOWS_WITH_MUTATION_LISTENERS",
     "IPC_SAME_PROCESS_MESSAGE_COPY_OOM_KB",
     "IPV4_AND_IPV6_ADDRESS_CONNECTIVITY",
     "JS_TELEMETRY_ADDON_EXCEPTIONS",
-    "LINK_ICON_SIZES_ATTR_DIMENSION",
-    "LINK_ICON_SIZES_ATTR_USAGE",
     "LOCALDOMSTORAGE_CLEAR_BLOCKING_MS",
     "LOCALDOMSTORAGE_GETALLKEYS_BLOCKING_MS",
     "LOCALDOMSTORAGE_GETKEY_BLOCKING_MS",
     "LOCALDOMSTORAGE_GETLENGTH_BLOCKING_MS",
     "LOCALDOMSTORAGE_GETVALUE_BLOCKING_MS",
     "LOCALDOMSTORAGE_PRELOAD_PENDING_ON_FIRST_ACCESS",
     "LOCALDOMSTORAGE_REMOVEKEY_BLOCKING_MS",
     "LOCALDOMSTORAGE_SESSIONONLY_PRELOAD_BLOCKING_MS",
@@ -366,20 +364,17 @@
     "NETWORK_DISK_CACHE_OPEN",
     "NETWORK_DISK_CACHE_SHUTDOWN",
     "NETWORK_DISK_CACHE_SHUTDOWN_CLEAR_PRIVATE",
     "NETWORK_DISK_CACHE_SHUTDOWN_V2",
     "NETWORK_DISK_CACHE_TRASHRENAME",
     "NEWTAB_PAGE_BLOCKED_SITES_COUNT",
     "NEWTAB_PAGE_ENABLED",
     "NEWTAB_PAGE_ENHANCED",
-    "NEWTAB_PAGE_LIFE_SPAN",
     "NEWTAB_PAGE_PINNED_SITES_COUNT",
-    "NEWTAB_PAGE_SHOWN",
-    "NEWTAB_PAGE_SITE_CLICKED",
     "NTLM_MODULE_USED_2",
     "ONBEFOREUNLOAD_PROMPT_ACTION",
     "ONBEFOREUNLOAD_PROMPT_COUNT",
     "OSFILE_WORKER_LAUNCH_MS",
     "OSFILE_WORKER_READY_MS",
     "OSFILE_WRITEATOMIC_JANK_MS",
     "PAGE_FAULTS_HARD",
     "PAINT_BUILD_DISPLAYLIST_TIME",
@@ -954,18 +949,16 @@
     "IMAGE_DECODE_SPEED_PNG",
     "IMAGE_DECODE_TIME",
     "INNERWINDOWS_WITH_MUTATION_LISTENERS",
     "IPC_SAME_PROCESS_MESSAGE_COPY_OOM_KB",
     "IPC_TRANSACTION_CANCEL",
     "IPV4_AND_IPV6_ADDRESS_CONNECTIVITY",
     "JS_DEPRECATED_LANGUAGE_EXTENSIONS_IN_CONTENT",
     "JS_TELEMETRY_ADDON_EXCEPTIONS",
-    "LINK_ICON_SIZES_ATTR_DIMENSION",
-    "LINK_ICON_SIZES_ATTR_USAGE",
     "LOCALDOMSTORAGE_CLEAR_BLOCKING_MS",
     "LOCALDOMSTORAGE_GETALLKEYS_BLOCKING_MS",
     "LOCALDOMSTORAGE_GETKEY_BLOCKING_MS",
     "LOCALDOMSTORAGE_GETLENGTH_BLOCKING_MS",
     "LOCALDOMSTORAGE_GETVALUE_BLOCKING_MS",
     "LOCALDOMSTORAGE_PRELOAD_PENDING_ON_FIRST_ACCESS",
     "LOCALDOMSTORAGE_REMOVEKEY_BLOCKING_MS",
     "LOCALDOMSTORAGE_SESSIONONLY_PRELOAD_BLOCKING_MS",
@@ -1054,20 +1047,17 @@
     "NETWORK_DISK_CACHE_OPEN",
     "NETWORK_DISK_CACHE_SHUTDOWN",
     "NETWORK_DISK_CACHE_SHUTDOWN_CLEAR_PRIVATE",
     "NETWORK_DISK_CACHE_SHUTDOWN_V2",
     "NETWORK_DISK_CACHE_TRASHRENAME",
     "NEWTAB_PAGE_BLOCKED_SITES_COUNT",
     "NEWTAB_PAGE_ENABLED",
     "NEWTAB_PAGE_ENHANCED",
-    "NEWTAB_PAGE_LIFE_SPAN",
     "NEWTAB_PAGE_PINNED_SITES_COUNT",
-    "NEWTAB_PAGE_SHOWN",
-    "NEWTAB_PAGE_SITE_CLICKED",
     "NTLM_MODULE_USED_2",
     "ONBEFOREUNLOAD_PROMPT_ACTION",
     "ONBEFOREUNLOAD_PROMPT_COUNT",
     "OSFILE_WORKER_LAUNCH_MS",
     "OSFILE_WORKER_READY_MS",
     "OSFILE_WRITEATOMIC_JANK_MS",
     "PAGE_FAULTS_HARD",
     "PAINT_BUILD_DISPLAYLIST_TIME",
@@ -1586,17 +1576,16 @@
     "SSL_PERMANENT_CERT_ERROR_OVERRIDES",
     "FX_THUMBNAILS_BG_QUEUE_SIZE_ON_CAPTURE",
     "AUTO_REJECTED_TRANSLATION_OFFERS",
     "TRANSLATED_CHARACTERS",
     "WEAVE_CONFIGURED",
     "NEWTAB_PAGE_ENABLED",
     "MOZ_SQLITE_OPEN_MS",
     "SHOULD_TRANSLATION_UI_APPEAR",
-    "NEWTAB_PAGE_LIFE_SPAN",
     "FX_TOTAL_TOP_VISITS",
     "FX_SESSION_RESTORE_NUMBER_OF_EAGER_TABS_RESTORED",
     "CACHE_DISK_SEARCH_2",
     "FX_THUMBNAILS_BG_CAPTURE_QUEUE_TIME_MS",
     "NEWTAB_PAGE_PINNED_SITES_COUNT",
     "WEAVE_COMPLETE_SUCCESS_COUNT",
     "OSFILE_WRITEATOMIC_JANK_MS",
     "STARTUP_MEASUREMENT_ERRORS",
--- a/toolkit/xre/nsEmbedFunctions.cpp
+++ b/toolkit/xre/nsEmbedFunctions.cpp
@@ -526,27 +526,29 @@ XRE_InitChildProcess(int aArgc,
 #ifdef OS_POSIX
   if (PR_GetEnv("MOZ_DEBUG_CHILD_PROCESS") ||
       PR_GetEnv("MOZ_DEBUG_CHILD_PAUSE")) {
 #if defined(XP_LINUX) && defined(DEBUG)
     if (prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0) != 0) {
       printf_stderr("Could not allow ptrace from any process.\n");
     }
 #endif
-    printf_stderr("\n\nCHILDCHILDCHILDCHILD\n  debug me @ %d\n\n",
+    printf_stderr("\n\nCHILDCHILDCHILDCHILD (process type %s)\n  debug me @ %d\n\n",
+                  XRE_ChildProcessTypeToString(XRE_GetProcessType()),
                   base::GetCurrentProcId());
     sleep(GetDebugChildPauseTime());
   }
 #elif defined(OS_WIN)
   if (PR_GetEnv("MOZ_DEBUG_CHILD_PROCESS")) {
     NS_DebugBreak(NS_DEBUG_BREAK,
                   "Invoking NS_DebugBreak() to debug child process",
                   nullptr, __FILE__, __LINE__);
   } else if (PR_GetEnv("MOZ_DEBUG_CHILD_PAUSE")) {
-    printf_stderr("\n\nCHILDCHILDCHILDCHILD\n  debug me @ %d\n\n",
+    printf_stderr("\n\nCHILDCHILDCHILDCHILD (process type %s)\n  debug me @ %d\n\n",
+                  XRE_ChildProcessTypeToString(XRE_GetProcessType()),
                   base::GetCurrentProcId());
     ::Sleep(GetDebugChildPauseTime());
   }
 #endif
 
   // child processes launched by GeckoChildProcessHost get this magic
   // argument appended to their command lines
   const char* const parentPIDString = aArgv[aArgc-1];
--- a/tools/lint/py2.yml
+++ b/tools/lint/py2.yml
@@ -29,16 +29,17 @@ py2:
         - probes/trace-gen.py
         - python/devtools
         - python/mach
         - python/mozbuild
         - python/mozversioncontrol
         - security
         - services/common/tests/mach_commands.py
         - servo
+        - taskcluster/docker/funsize-update-generator
         - testing/awsy
         - testing/firefox-ui
         - testing/geckodriver
         - testing/gtest
         - testing/marionette
         - testing/mochitest
         - testing/mozharness
         - testing/remotecppunittests.py