Repository: GoogleChrome/lighthouse
Pull request #8961 (2019-05-15 16:17:24)

core(report): show nodeLabel for DOM nodes in addition to snippet
**Summary**

Collect a human-readable title for each node and display it above the snippet.

![Screenshot 2019-05-16 at 14 41 28](https://user-images.githubusercontent.com/1303660/57858333-bf78e500-77e8-11e9-83d0-f6f2b225b0b2.png)

**Related Issues/PRs**

https://github.com/GoogleChrome/lighthouse/issues/6683


mattzeunert: > I wonder if there's more we could do to tie the new text and the snippet together visually, though

Removed some unnecessary margin between the two. Not sure what else to do exactly, @hwikyounglee any suggestions?
hwikyounglee: > > I wonder if there's more we could do to tie the new text and the snippet together visually, though
> 
> Removed some unnecessary margin between the two. Not sure what else to do exactly, @hwikyounglee any suggestions?

Thanks Matt. Only thing I would double check is the contrast of the green text on gray and on white. We need to aim for 4.5 at minimum. 

@yuinchien for any further guidance
mattzeunert: > Only thing I would double check is the contrast of the green text on gray and on white. We need to aim for 4.5 at minimum.

Good point. We weren't using a var for it before, using `--color-green-700` for now:

![Screenshot 2019-05-16 at 17 11 54](https://user-images.githubusercontent.com/1303660/57869859-d5dd6b80-77fd-11e9-90da-69d68a225b2d.png)


Patch:
diff --git a/.travis.yml b/.travis.yml
index d99b65ea5..e8ec33ea7 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -40,14 +40,7 @@ script:
   - yarn test-viewer
   - yarn test-lantern
   - yarn i18n:checks
-  # _JAVA_OPTIONS is breaking parsing of `compile-devtools` output. See #3338.
-  - unset _JAVA_OPTIONS
-  # FIXME(paulirish): re-enable after a roll of LH->CDT
-  # - yarn compile-devtools
 before_cache:
-  # the `yarn compile-devtools` task adds these to node_modules, which slows down caching
-  - rm -rf ./node_modules/temp-devtoolsfrontend/
-  - rm -rf ./node_modules/temp-devtoolsprotocol/
   # nyc, jest and other projects store files in here. They mess up the travis build cache.
   - rm -rf ./node_modules/.cache/
 after_success:
diff --git a/lighthouse-core/audits/accessibility/axe-audit.js b/lighthouse-core/audits/accessibility/axe-audit.js
index 56d418e84..b2f9c210a 100644
--- a/lighthouse-core/audits/accessibility/axe-audit.js
+++ b/lighthouse-core/audits/accessibility/axe-audit.js
@@ -54,6 +54,7 @@ class AxeAudit extends Audit {
           path: node.path,
           snippet: node.html || node.snippet,
           explanation: node.failureSummary,
+          nodeLabel: node.nodeLabel,
         }),
       }));
     }
diff --git a/lighthouse-core/audits/seo/tap-targets.js b/lighthouse-core/audits/seo/tap-targets.js
index 25afeaaf9..29009a568 100644
--- a/lighthouse-core/audits/seo/tap-targets.js
+++ b/lighthouse-core/audits/seo/tap-targets.js
@@ -251,6 +251,7 @@ function targetToTableNode(target) {
     snippet: target.snippet,
     path: target.path,
     selector: target.selector,
+    nodeLabel: target.nodeLabel,
   };
 }
 
diff --git a/lighthouse-core/config/budget.js b/lighthouse-core/config/budget.js
index e788e8f49..0f5d1553a 100644
--- a/lighthouse-core/config/budget.js
+++ b/lighthouse-core/config/budget.js
@@ -46,6 +46,22 @@ class Budget {
     }
   }
 
+  /**
+   * Asserts that `strings` has no duplicate strings in it, throwing an error if
+   * it does. `arrayName` is included for nicer logging.
+   * @param {Array<string>} strings
+   * @param {string} arrayName
+   */
+  static assertNoDuplicateStrings(strings, arrayName) {
+    const foundStrings = new Set();
+    for (const string of strings) {
+      if (foundStrings.has(string)) {
+        throw new Error(`${arrayName} has duplicate entry of type '${string}'`);
+      }
+      foundStrings.add(string);
+    }
+  }
+
   /**
    * @param {Record<string, unknown>} resourceBudget
    * @return {LH.Budget.ResourceBudget}
@@ -94,7 +110,7 @@ class Budget {
       'first-cpu-idle',
       'interactive',
       'first-meaningful-paint',
-      'estimated-input-latency',
+      'max-potential-fid',
     ];
     // Assume metric is an allowed string, throw if not.
     if (!validTimingMetrics.includes(/** @type {LH.Budget.TimingMetric} */ (metric))) {
@@ -136,18 +152,24 @@ class Budget {
 
       if (isArrayOfUnknownObjects(resourceSizes)) {
         budget.resourceSizes = resourceSizes.map(Budget.validateResourceBudget);
+        Budget.assertNoDuplicateStrings(budget.resourceSizes.map(r => r.resourceType),
+          `budgets[${index}].resourceSizes`);
       } else if (resourceSizes !== undefined) {
         throw new Error(`Invalid resourceSizes entry in budget at index ${index}`);
       }
 
       if (isArrayOfUnknownObjects(resourceCounts)) {
         budget.resourceCounts = resourceCounts.map(Budget.validateResourceBudget);
+        Budget.assertNoDuplicateStrings(budget.resourceCounts.map(r => r.resourceType),
+          `budgets[${index}].resourceCounts`);
       } else if (resourceCounts !== undefined) {
         throw new Error(`Invalid resourceCounts entry in budget at index ${index}`);
       }
 
       if (isArrayOfUnknownObjects(timings)) {
         budget.timings = timings.map(Budget.validateTimingBudget);
+        Budget.assertNoDuplicateStrings(budget.timings.map(r => r.metric),
+          `budgets[${index}].timings`);
       } else if (timings !== undefined) {
         throw new Error(`Invalid timings entry in budget at index ${index}`);
       }
diff --git a/lighthouse-core/gather/gatherers/accessibility.js b/lighthouse-core/gather/gatherers/accessibility.js
index 57cb71cfb..9d0e9165f 100644
--- a/lighthouse-core/gather/gatherers/accessibility.js
+++ b/lighthouse-core/gather/gatherers/accessibility.js
@@ -5,7 +5,7 @@
  */
 'use strict';
 
-/* global window, document, getOuterHTMLSnippet, getNodePath */
+/* global window, document, getOuterHTMLSnippet, getNodePath, getNodeLabel */
 
 const Gatherer = require('./gatherer');
 const fs = require('fs');
@@ -57,6 +57,8 @@ function runA11yChecks() {
       node.path = getNodePath(node.element);
       // @ts-ignore - getOuterHTMLSnippet put into scope via stringification
       node.snippet = getOuterHTMLSnippet(node.element);
+      // @ts-ignore - getNodeLabel put into scope via stringification
+      node.nodeLabel = getNodeLabel(node.element);
       // avoid circular JSON concerns
       node.element = node.any = node.all = node.none = undefined;
     }));
@@ -77,6 +79,7 @@ class Accessibility extends Gatherer {
     const expression = `(function () {
       ${pageFunctions.getOuterHTMLSnippetString};
       ${pageFunctions.getNodePathString};
+      ${pageFunctions.getNodeLabelString};
       ${axeLibSource};
       return (${runA11yChecks.toString()}());
     })()`;
diff --git a/lighthouse-core/gather/gatherers/seo/tap-targets.js b/lighthouse-core/gather/gatherers/seo/tap-targets.js
index 6314c60f7..6e5591782 100644
--- a/lighthouse-core/gather/gatherers/seo/tap-targets.js
+++ b/lighthouse-core/gather/gatherers/seo/tap-targets.js
@@ -5,7 +5,7 @@
  */
 'use strict';
 
-/* global getComputedStyle, getElementsInDocument, Node, getNodePath, getNodeSelector */
+/* global getComputedStyle, getElementsInDocument, Node, getNodePath, getNodeSelector, getNodeLabel */
 
 const Gatherer = require('../gatherer');
 const pageFunctions = require('../../../lib/page-functions.js');
@@ -239,7 +239,7 @@ function elementIsPositionFixedStickyOrAbsolute(element) {
 /**
  * @param {string} str
  * @param {number} maxLength
- * @returns {string}
+ * @return {string}
  */
 /* istanbul ignore next */
 function truncate(str, maxLength) {
@@ -294,6 +294,8 @@ function gatherTapTargets() {
       path: getNodePath(tapTargetElement),
       // @ts-ignore - getNodeSelector put into scope via stringification
       selector: getNodeSelector(tapTargetElement),
+      // @ts-ignore - getNodeLabel put into scope via stringification
+      nodeLabel: getNodeLabel(tapTargetElement),
       href: /** @type {HTMLAnchorElement} */(tapTargetElement)['href'] || '',
     });
   });
@@ -323,6 +325,7 @@ class TapTargets extends Gatherer {
       ${rectContainsString};
       ${pageFunctions.getNodePathString};
       ${pageFunctions.getNodeSelectorString};
+      ${pageFunctions.getNodeLabelString};
       ${gatherTapTargets.toString()};
 
       return gatherTapTargets();
diff --git a/lighthouse-core/lib/minify-devtoolslog.js b/lighthouse-core/lib/minify-devtoolslog.js
new file mode 100644
index 000000000..4f6e9451e
--- /dev/null
+++ b/lighthouse-core/lib/minify-devtoolslog.js
@@ -0,0 +1,87 @@
+/**
+ * @license Copyright 2019 Google Inc. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+'use strict';
+
+/* eslint-disable no-console */
+
+/**
+ * @fileoverview Minifies a devtools log by removing noisy header values, eliminating data URIs, etc.
+ */
+
+const headersToKeep = new Set([
+  // Request headers
+  'accept',
+  'accept-encoding',
+  'accept-ranges',
+  // Response headers
+  'status',
+  'content-length',
+  'content-type',
+  'content-encoding',
+  'content-range',
+  'etag',
+  'cache-control',
+  'last-modified',
+  'link',
+  'x-robots-tag',
+]);
+
+/** @param {LH.Crdp.Network.Headers} [headers] */
+function cleanHeaders(headers) {
+  if (!headers) return;
+
+  for (const key of Object.keys(headers)) {
+    if (!headersToKeep.has(key.toLowerCase())) delete headers[key];
+  }
+}
+
+/** @param {{url: string}} obj */
+function cleanDataURI(obj) {
+  obj.url = obj.url.replace(/^(data:.*?base64,).*/, '$1FILLER');
+}
+
+/** @param {LH.Crdp.Network.Response} [response] */
+function cleanResponse(response) {
+  if (!response) return;
+  cleanDataURI(response);
+  cleanHeaders(response.requestHeaders);
+  cleanHeaders(response.headers);
+  response.securityDetails = undefined;
+  response.headersText = undefined;
+  response.requestHeadersText = undefined;
+
+  /** @type {any} */
+  const timing = response.timing || {};
+  for (const [k, v] of Object.entries(timing)) {
+    if (v === -1) timing[k] = undefined;
+  }
+}
+
+/**
+ * @param {LH.DevtoolsLog} log
+ * @return {LH.DevtoolsLog}
+ */
+function minifyDevtoolsLog(log) {
+  return log.map(original => {
+    /** @type {LH.Protocol.RawEventMessage} */
+    const entry = JSON.parse(JSON.stringify(original));
+
+    switch (entry.method) {
+      case 'Network.requestWillBeSent':
+        cleanDataURI(entry.params.request);
+        cleanHeaders(entry.params.request.headers);
+        cleanResponse(entry.params.redirectResponse);
+        break;
+      case 'Network.responseReceived':
+        cleanResponse(entry.params.response);
+        break;
+    }
+
+    return entry;
+  });
+}
+
+module.exports = {minifyDevtoolsLog};
diff --git a/lighthouse-core/lib/minify-trace.js b/lighthouse-core/lib/minify-trace.js
new file mode 100644
index 000000000..c6610a760
--- /dev/null
+++ b/lighthouse-core/lib/minify-trace.js
@@ -0,0 +1,149 @@
+/**
+ * @license Copyright 2019 Google Inc. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+'use strict';
+
+/* eslint-disable no-console */
+
+/**
+ * @fileoverview Minifies a trace by removing unnecessary events, throttling screenshots, etc.
+ *  See the following files for necessary events:
+ *    - lighthouse-core/computed/trace-of-tab.js
+ *    - lighthouse-core/computed/page-dependency-graph.js
+ *    - lighthouse-core/lib/dependency-graph/cpu-node.js
+ *    - lighthouse-core/lib/traces/tracing-processor.js
+ */
+
+const TracingProcessor = require('./traces/tracing-processor.js');
+
+const toplevelTaskNames = new Set([
+  'RunTask', // m71+
+  'ThreadControllerImpl::RunTask', // m69-70
+  'ThreadControllerImpl::DoWork', // m66-68
+  'TaskQueueManager::ProcessTaskFromWorkQueue', // m65 and below
+]);
+
+const traceEventsToAlwaysKeep = new Set([
+  'Screenshot',
+  'TracingStartedInBrowser',
+  'TracingStartedInPage',
+  'navigationStart',
+  'ParseAuthorStyleSheet',
+  'ParseHTML',
+  'PlatformResourceSendRequest',
+  'ResourceSendRequest',
+  'ResourceReceiveResponse',
+  'ResourceFinish',
+  'ResourceReceivedData',
+  'EventDispatch',
+]);
+
+const traceEventsToKeepInToplevelTask = new Set([
+  // Needed for CPU node timing simulations
+  'Layout',
+  // All of these are needed to create graph relationships
+  'TimerInstall',
+  'TimerFire',
+  'InvalidateLayout',
+  'ScheduleStyleRecalculation',
+  'EvaluateScript',
+  'XHRReadyStateChange',
+  'FunctionCall',
+  'v8.compile',
+  'ParseAuthorStyleSheet',
+  'ResourceSendRequest',
+]);
+
+const traceEventsToKeepInProcess = new Set([
+  ...toplevelTaskNames,
+  ...traceEventsToKeepInToplevelTask,
+  'firstPaint',
+  'firstContentfulPaint',
+  'firstMeaningfulPaint',
+  'firstMeaningfulPaintCandidate',
+  'loadEventEnd',
+  'domContentLoadedEventEnd',
+]);
+
+/**
+ * @param {LH.TraceEvent[]} events
+ */
+function filterOutUnnecessaryTasksByNameAndDuration(events) {
+  const {pid} = TracingProcessor.findMainFrameIds(events);
+
+  return events.filter(evt => {
+    if (toplevelTaskNames.has(evt.name) && evt.dur < 1000) return false;
+    if (evt.pid === pid && traceEventsToKeepInProcess.has(evt.name)) return true;
+    return traceEventsToAlwaysKeep.has(evt.name);
+  });
+}
+
+/**
+ * Filters out tasks that are not within a toplevel task.
+ * @param {LH.TraceEvent[]} events
+ */
+function filterOutOrphanedTasks(events) {
+  const toplevelRanges = events
+    .filter(evt => toplevelTaskNames.has(evt.name))
+    .map(evt => [evt.ts, evt.ts + evt.dur]);
+
+  /** @param {LH.TraceEvent} e */
+  const isInToplevelTask = e => toplevelRanges.some(([start, end]) => e.ts >= start && e.ts <= end);
+
+  return events.filter((evt, index) => {
+    if (!traceEventsToKeepInToplevelTask.has(evt.name)) return true;
+    if (!isInToplevelTask(evt)) return false;
+
+    if (evt.ph === 'B') {
+      const endEvent = events.slice(index).find(e => e.name === evt.name && e.ph === 'E');
+      return endEvent && isInToplevelTask(endEvent);
+    } else {
+      return true;
+    }
+  });
+}
+
+/**
+ * Throttles screenshot events in the trace to 2fps.
+ * @param {LH.TraceEvent[]} events
+ */
+function filterOutExcessiveScreenshots(events) {
+  const screenshotTimestamps = events.filter(evt => evt.name === 'Screenshot').map(evt => evt.ts);
+
+  let lastScreenshotTs = -Infinity;
+  return events.filter(evt => {
+    if (evt.name !== 'Screenshot') return true;
+    const timeSinceLastScreenshot = evt.ts - lastScreenshotTs;
+    const nextScreenshotTs = screenshotTimestamps.find(ts => ts > evt.ts);
+    const timeUntilNextScreenshot = nextScreenshotTs ? nextScreenshotTs - evt.ts : Infinity;
+    const threshold = 500 * 1000; // Throttle to ~2fps
+    // Keep the frame if it's been more than 500ms since the last frame we kept or the next frame won't happen for at least 500ms
+    const shouldKeep = timeUntilNextScreenshot > threshold || timeSinceLastScreenshot > threshold;
+    if (shouldKeep) lastScreenshotTs = evt.ts;
+    return shouldKeep;
+  });
+}
+
+/**
+ * @param {LH.TraceEvent[]} events
+ */
+function filterTraceEvents(events) {
+  // Filter out event names we don't care about and tasks <1ms
+  let filtered = filterOutUnnecessaryTasksByNameAndDuration(events);
+  // Filter out events not inside a toplevel task
+  filtered = filterOutOrphanedTasks(filtered);
+  // Filter down the screenshots to key moments + 2fps animations
+  return filterOutExcessiveScreenshots(filtered);
+}
+
+/**
+ * @param {LH.Trace} inputTrace
+ * @return {LH.Trace}
+ */
+function minifyTrace(inputTrace) {
+  return {traceEvents: filterTraceEvents(inputTrace.traceEvents)};
+}
+
+module.exports = {minifyTrace};
diff --git a/lighthouse-core/lib/page-functions.js b/lighthouse-core/lib/page-functions.js
index 9e9b79d69..4a09367b8 100644
--- a/lighthouse-core/lib/page-functions.js
+++ b/lighthouse-core/lib/page-functions.js
@@ -187,7 +187,7 @@ function getNodePath(node) {
 
 /**
  * @param {Element} node
- * @returns {string}
+ * @return {string}
  */
 /* istanbul ignore next */
 function getNodeSelector(node) {
@@ -218,6 +218,47 @@ function getNodeSelector(node) {
   return parts.join(' > ');
 }
 
+/**
+ * Generate a human-readable label for the given element, based on end-user facing
+ * strings like the innerText or alt attribute.
+ * Falls back to the tagName if no useful label is found.
+ * @param {HTMLElement} node
+ * @return {string|null}
+ */
+/* istanbul ignore next */
+function getNodeLabel(node) {
+  // Inline so that audits that import getNodeLabel don't
+  // also need to import truncate
+  /**
+   * @param {string} str
+   * @param {number} maxLength
+   * @return {string}
+   */
+  function truncate(str, maxLength) {
+    if (str.length <= maxLength) {
+      return str;
+    }
+    return str.slice(0, maxLength - 1) + '…';
+  }
+
+  const tagName = node.tagName.toLowerCase();
+  // html and body content is too broad to be useful, since they contain all page content
+  if (tagName !== 'html' && tagName !== 'body') {
+    const nodeLabel = node.innerText || node.getAttribute('alt') || node.getAttribute('aria-label');
+    if (nodeLabel) {
+      return truncate(nodeLabel, 80);
+    } else {
+      // If no useful label was found then try to get one from a child.
+      // E.g. if an a tag contains an image but no text we want the image alt/aria-label attribute.
+      const nodeToUseForLabel = node.querySelector('[alt], [aria-label]');
+      if (nodeToUseForLabel) {
+        return getNodeLabel(/** @type {HTMLElement} */ (nodeToUseForLabel));
+      }
+    }
+  }
+  return tagName;
+}
+
 module.exports = {
   wrapRuntimeEvalErrorInBrowserString: wrapRuntimeEvalErrorInBrowser.toString(),
   registerPerformanceObserverInPageString: registerPerformanceObserverInPage.toString(),
@@ -230,4 +271,6 @@ module.exports = {
   getNodePathString: getNodePath.toString(),
   getNodeSelectorString: getNodeSelector.toString(),
   getNodeSelector: getNodeSelector,
+  getNodeLabel: getNodeLabel,
+  getNodeLabelString: getNodeLabel.toString(),
 };
diff --git a/lighthouse-core/report/html/renderer/details-renderer.js b/lighthouse-core/report/html/renderer/details-renderer.js
index 67ac6b389..d7cb6abe0 100644
--- a/lighthouse-core/report/html/renderer/details-renderer.js
+++ b/lighthouse-core/report/html/renderer/details-renderer.js
@@ -352,8 +352,16 @@ class DetailsRenderer {
    */
   renderNode(item) {
     const element = this._dom.createElement('span', 'lh-node');
+    if (item.nodeLabel) {
+      const nodeLabelEl = this._dom.createElement('div');
+      nodeLabelEl.textContent = item.nodeLabel;
+      element.appendChild(nodeLabelEl);
+    }
     if (item.snippet) {
-      element.textContent = item.snippet;
+      const snippetEl = this._dom.createElement('div');
+      snippetEl.classList.add('lh-node__snippet');
+      snippetEl.textContent = item.snippet;
+      element.appendChild(snippetEl);
     }
     if (item.selector) {
       element.title = item.selector;
@@ -361,6 +369,7 @@ class DetailsRenderer {
     if (item.path) element.setAttribute('data-path', item.path);
     if (item.selector) element.setAttribute('data-selector', item.selector);
     if (item.snippet) element.setAttribute('data-snippet', item.snippet);
+
     return element;
   }
 
diff --git a/lighthouse-core/report/html/renderer/report-ui-features.js b/lighthouse-core/report/html/renderer/report-ui-features.js
index 2458fa5e6..bfbdc7e06 100644
--- a/lighthouse-core/report/html/renderer/report-ui-features.js
+++ b/lighthouse-core/report/html/renderer/report-ui-features.js
@@ -173,9 +173,10 @@ class ReportUIFeatures {
       });
 
     tablesWithUrls.forEach((tableEl, index) => {
-      const thirdPartyRows = this._getThirdPartyRows(tableEl, this.json.finalUrl);
-      // No 3rd parties, no checkbox!
-      if (!thirdPartyRows.size) return;
+      const urlItems = this._getUrlItems(tableEl);
+      const thirdPartyRows = this._getThirdPartyRows(tableEl, urlItems, this.json.finalUrl);
+      // If all or none of the rows are 3rd party, no checkbox!
+      if (thirdPartyRows.size === urlItems.length || !thirdPartyRows.size) return;
 
       // create input box
       const filterTemplate = this._dom.cloneTemplate('#tmpl-lh-3p-filter', this._document);
@@ -216,10 +217,10 @@ class ReportUIFeatures {
    * and returns a Map of those rows, mapping from row index to row Element.
    * @param {HTMLTableElement} el
    * @param {string} finalUrl
+   * @param {Array<HTMLElement>} urlItems
    * @return {Map<number, HTMLTableRowElement>}
    */
-  _getThirdPartyRows(el, finalUrl) {
-    const urlItems = this._dom.findAll('.lh-text__url', el);
+  _getThirdPartyRows(el, urlItems, finalUrl) {
     const finalUrlRootDomain = Util.getRootDomain(finalUrl);
 
     /** @type {Map<number, HTMLTableRowElement>} */
@@ -240,6 +241,15 @@ class ReportUIFeatures {
     return thirdPartyRows;
   }
 
+  /**
+   * From a table, finds and returns URL items.
+   * @param {HTMLTableElement} tableEl
+   * @return {Array<HTMLElement>}
+   */
+  _getUrlItems(tableEl) {
+    return this._dom.findAll('.lh-text__url', tableEl);
+  }
+
   _setupStickyHeaderElements() {
     this.topbarEl = this._dom.find('.lh-topbar', this._document);
     this.scoreScaleEl = this._dom.find('.lh-scorescale', this._document);
diff --git a/lighthouse-core/report/html/report-styles.css b/lighthouse-core/report/html/report-styles.css
index 0ba4ca75d..be3769bc1 100644
--- a/lighthouse-core/report/html/report-styles.css
+++ b/lighthouse-core/report/html/report-styles.css
@@ -80,7 +80,8 @@
   --chevron-size: 12px;
   --inner-audit-left-padding: calc(var(--score-shape-size) + var(--score-shape-margin-left) + var(--score-shape-margin-right));
 
-  /* Palette. */
+  /* Palette using Material Design Colors
+   * https://www.materialui.co/colors */
   --color-black-100: #F5F5F5;
   --color-black-200: #E0E0E0;
   --color-black-400: #BDBDBD;
@@ -100,6 +101,8 @@
   --color-white: #FFFFFF;
   --color-blue-200: #90CAF9;
   --color-blue-900: #0D47A1;
+  --color-cyan-500: #00BCD4;
+  --color-teal-600: #00897B;
 
 
   /* TODO(cjamcl) clean up unused variables. */
@@ -175,6 +178,7 @@
 .lh-vars.dark {
   --color-red-700: var(--color-red);
   --color-green-700: var(--color-green);
+  --color-teal-600: var(--color-cyan-500);
   --color-orange-700: var(--color-orange);
 
   --color-black-200: var(--color-black-800);
@@ -388,10 +392,11 @@
 }
 
 /* Node */
-.lh-node {
-  display: block;
+.lh-node__snippet {
   font-family: var(--monospace-font-family);
-  color: hsl(174, 100%, 27%);
+  color: var(--color-teal-600);
+  font-size: 12px;
+  line-height: 1.5em;
 }
 
 /* Score */
@@ -1022,10 +1027,134 @@
   border: 0;
 }
 
+/* Gauge */
+
+.lh-gauge__wrapper--pass {
+  color: var(--color-pass);
+  fill: var(--color-pass);
+  stroke: var(--color-pass);
+}
+
+.lh-gauge__wrapper--average {
+  color: var(--color-average);
+  fill: var(--color-average);
+  stroke: var(--color-average);
+}
+
+.lh-gauge__wrapper--fail {
+  color: var(--color-fail);
+  fill: var(--color-fail);
+  stroke: var(--color-fail);
+}
+
+.lh-gauge {
+  stroke-linecap: round;
+  width: var(--gauge-circle-size);
+  height: var(--gauge-circle-size);
+}
+
+.lh-category .lh-gauge {
+  --gauge-circle-size: var(--gauge-circle-size-big);
+}
+
+.lh-gauge-base {
+    opacity: 0.1;
+    stroke: var(--circle-background);
+    stroke-width: var(--circle-border-width);
+}
+
+.lh-gauge-arc {
+    fill: none;
+    stroke: var(--circle-color);
+    stroke-width: var(--circle-border-width);
+    animation: load-gauge var(--transition-length) ease forwards;
+    animation-delay: 250ms;
+}
+
+.lh-gauge__svg-wrapper {
+  position: relative;
+  height: var(--gauge-circle-size);
+}
+.lh-category .lh-gauge__svg-wrapper {
+  --gauge-circle-size: var(--gauge-circle-size-big);
+}
+
+/* The plugin badge overlay */
+.lh-gauge__wrapper--plugin .lh-gauge__svg-wrapper::before {
+  width: var(--plugin-badge-size);
+  height: var(--plugin-badge-size);
+  background-color: var(--plugin-badge-bg);
+  background-image: var(--plugin-icon-url);
+  background-repeat: no-repeat;
+  background-size: var(--plugin-icon-size);
+  background-position: 58% 50%;
+  content: "";
+  position: absolute;
+  right: -6px;
+  bottom: 0px;
+  display: block;
+  z-index: 100;
+  box-shadow: 0 0 4px rgba(0,0,0,.2);
+  border-radius: 25%;
+}
+.lh-category .lh-gauge__wrapper--plugin .lh-gauge__svg-wrapper::before {
+  width: var(--plugin-badge-size-big);
+  height: var(--plugin-badge-size-big);
+}
+
+@keyframes load-gauge {
+  from { stroke-dasharray: 0 352; }
+}
+
+.lh-gauge__percentage {
+  width: 100%;
+  height: var(--inset-size);
+  position: absolute;
+  border-radius: inherit;
+  font-family: var(--monospace-font-family);
+  font-size: var(--score-number-font-size);
+  line-height: var(--score-number-font-size);
+  text-align: center;
+  top: calc(var(--score-container-padding) + var(--gauge-circle-size) / 3);
+}
+
+.lh-category .lh-gauge__percentage {
+  --gauge-circle-size: var(--gauge-circle-size-big);
+  --score-number-font-size: var(--score-number-font-size-big);
+}
+
 .lh-gauge__wrapper {
+  position: relative;
+  display: flex;
+  align-items: center;
+  flex-direction: column;
+  text-decoration: none;
   padding: var(--score-container-padding);
+
+  --circle-border-width: 8;
+  --transition-length: 1s;
+
+  /* Contain the layout style paint & layers during animation*/
+  contain: content;
+  will-change: opacity; /* Only using for layer promotion */
+}
+
+.lh-gauge__label {
+  font-size: var(--score-title-font-size);
+  line-height: var(--score-title-line-height);
+  margin-top: 10px;
+  text-align: center;
+  color: var(--body-text-color);
 }
 
+/* TODO(#8185) use more BEM (.lh-gauge__label--big) instead of relying on descendant selector */
+.lh-category .lh-gauge__label {
+  --score-title-font-size: var(--score-title-font-size-big);
+  --score-title-line-height: var(--score-title-line-height-big);
+  margin-top: 14px;
+}
+
+
 .lh-scores-header .lh-gauge__wrapper,
 .lh-scores-header .lh-gauge--pwa__wrapper,
 .lh-sticky-header .lh-gauge__wrapper,
diff --git a/lighthouse-core/report/html/templates.html b/lighthouse-core/report/html/templates.html
index e46c5a977..cc8d7c798 100644
--- a/lighthouse-core/report/html/templates.html
+++ b/lighthouse-core/report/html/templates.html
@@ -539,132 +539,6 @@ limitations under the License.
 
 <!-- Lighthouse score gauge -->
 <template id="tmpl-lh-gauge">
-  <style>
-    .lh-gauge__wrapper--pass {
-      color: var(--color-pass);
-      fill: var(--color-pass);
-      stroke: var(--color-pass);
-    }
-
-    .lh-gauge__wrapper--average {
-      color: var(--color-average);
-      fill: var(--color-average);
-      stroke: var(--color-average);
-    }
-
-    .lh-gauge__wrapper--fail {
-      color: var(--color-fail);
-      fill: var(--color-fail);
-      stroke: var(--color-fail);
-    }
-
-    .lh-gauge {
-      stroke-linecap: round;
-      width: var(--gauge-circle-size);
-      height: var(--gauge-circle-size);
-    }
-
-    .lh-category .lh-gauge {
-      --gauge-circle-size: var(--gauge-circle-size-big);
-    }
-
-    .lh-gauge-base {
-        opacity: 0.1;
-        stroke: var(--circle-background);
-        stroke-width: var(--circle-border-width);
-    }
-
-    .lh-gauge-arc {
-        fill: none;
-        stroke: var(--circle-color);
-        stroke-width: var(--circle-border-width);
-        animation: load-gauge var(--transition-length) ease forwards;
-        animation-delay: 250ms;
-    }
-
-    .lh-gauge__svg-wrapper {
-      position: relative;
-      height: var(--gauge-circle-size);
-    }
-    .lh-category .lh-gauge__svg-wrapper {
-      --gauge-circle-size: var(--gauge-circle-size-big);
-    }
-
-    /* The plugin badge overlay */
-    .lh-gauge__wrapper--plugin .lh-gauge__svg-wrapper::before {
-      width: var(--plugin-badge-size);
-      height: var(--plugin-badge-size);
-      background-color: var(--plugin-badge-bg);
-      background-image: var(--plugin-icon-url);
-      background-repeat: no-repeat;
-      background-size: var(--plugin-icon-size);
-      background-position: 58% 50%;
-      content: "";
-      position: absolute;
-      right: -6px;
-      bottom: 0px;
-      display: block;
-      z-index: 100;
-      box-shadow: 0 0 4px rgba(0,0,0,.2);
-      border-radius: 25%;
-    }
-    .lh-category .lh-gauge__wrapper--plugin .lh-gauge__svg-wrapper::before {
-      width: var(--plugin-badge-size-big);
-      height: var(--plugin-badge-size-big);
-    }
-
-    @keyframes load-gauge {
-      from { stroke-dasharray: 0 352; }
-    }
-
-    .lh-gauge__percentage {
-      width: 100%;
-      height: var(--inset-size);
-      position: absolute;
-      border-radius: inherit;
-      font-family: var(--monospace-font-family);
-      font-size: var(--score-number-font-size);
-      line-height: var(--score-number-font-size);
-      text-align: center;
-      top: calc(var(--score-container-padding) + var(--gauge-circle-size) / 3);
-    }
-
-    .lh-category .lh-gauge__percentage {
-      --gauge-circle-size: var(--gauge-circle-size-big);
-      --score-number-font-size: var(--score-number-font-size-big);
-    }
-
-    .lh-gauge__wrapper {
-      position: relative;
-      display: flex;
-      align-items: center;
-      flex-direction: column;
-      text-decoration: none;
-
-      --circle-border-width: 8;
-      --transition-length: 1s;
-
-      /* Contain the layout style paint & layers during animation*/
-      contain: content;
-      will-change: opacity; /* Only using for layer promotion */
-    }
-
-    .lh-gauge__label {
-      font-size: var(--score-title-font-size);
-      line-height: var(--score-title-line-height);
-      margin-top: 10px;
-      text-align: center;
-      color: var(--body-text-color);
-    }
-
-    /* TODO(#8185) use more BEM (.lh-gauge__label--big) instead of relying on descendant selector */
-    .lh-category .lh-gauge__label {
-      --score-title-font-size: var(--score-title-font-size-big);
-      --score-title-line-height: var(--score-title-line-height-big);
-      margin-top: 14px;
-    }
-
-  </style>
   <a href="#" class="lh-gauge__wrapper">
     <!-- Wrapper exists for the ::before plugin icon. Cannot create pseudo-elements on svgs. -->
     <div class="lh-gauge__svg-wrapper">
diff --git a/lighthouse-core/scripts/lantern/minify-devtoolslog.js b/lighthouse-core/scripts/lantern/minify-devtoolslog.js
index 878e5243e..d1e6b3a1b 100755
--- a/lighthouse-core/scripts/lantern/minify-devtoolslog.js
+++ b/lighthouse-core/scripts/lantern/minify-devtoolslog.js
@@ -14,6 +14,7 @@
 
 const fs = require('fs');
 const path = require('path');
+const {minifyDevtoolsLog} = require('../../lib/minify-devtoolslog.js');
 
 if (process.argv.length !== 4) {
   console.error('Usage $0: <input file> <output file>');
@@ -26,79 +27,9 @@ const inputDevtoolsLogRaw = fs.readFileSync(inputDevtoolsLogPath, 'utf8');
 /** @type {LH.DevtoolsLog} */
 const inputDevtoolsLog = JSON.parse(inputDevtoolsLogRaw);
 
-const headersToKeep = new Set([
-  // Request headers
-  'accept',
-  'accept-encoding',
-  'accept-ranges',
-  // Response headers
-  'status',
-  'content-length',
-  'content-type',
-  'content-encoding',
-  'content-range',
-  'etag',
-  'cache-control',
-  'last-modified',
-  'link',
-  'x-robots-tag',
-]);
-
-/** @param {Partial<LH.Crdp.Network.Headers>} [headers] */
-function cleanHeaders(headers) {
-  if (!headers) return;
-
-  for (const key of Object.keys(headers)) {
-    if (!headersToKeep.has(key.toLowerCase())) headers[key] = undefined;
-  }
-}
-
-/** @param {{url: string}} obj */
-function cleanDataURI(obj) {
-  obj.url = obj.url.replace(/^(data:.*?base64,).*/, '$1FILLER');
-}
-
-/** @param {LH.Crdp.Network.Response} [response] */
-function cleanResponse(response) {
-  if (!response) return;
-  cleanDataURI(response);
-  cleanHeaders(response.requestHeaders);
-  cleanHeaders(response.headers);
-  response.securityDetails = undefined;
-  response.headersText = undefined;
-  response.requestHeadersText = undefined;
-
-  /** @type {any} */
-  const timing = response.timing || {};
-  for (const [k, v] of Object.entries(timing)) {
-    if (v === -1) timing[k] = undefined;
-  }
-}
-
-/** @param {LH.DevtoolsLog} log */
-function filterDevtoolsLogEvents(log) {
-  return log.map(original => {
-    /** @type {LH.Protocol.RawEventMessage} */
-    const entry = JSON.parse(JSON.stringify(original));
-
-    switch (entry.method) {
-      case 'Network.requestWillBeSent':
-        cleanDataURI(entry.params.request);
-        cleanHeaders(entry.params.request.headers);
-        cleanResponse(entry.params.redirectResponse);
-        break;
-      case 'Network.responseReceived':
-        cleanResponse(entry.params.response);
-        break;
-    }
-
-    return entry;
-  });
-}
-
-const filteredLog = filterDevtoolsLogEvents(inputDevtoolsLog);
+const outputDevtoolsLog = minifyDevtoolsLog(inputDevtoolsLog);
 const output = `[
-${filteredLog.map(e => '  ' + JSON.stringify(e)).join(',\n')}
+${outputDevtoolsLog.map(e => '  ' + JSON.stringify(e)).join(',\n')}
 ]`;
 
 /** @param {string} s */
diff --git a/lighthouse-core/scripts/lantern/minify-trace.js b/lighthouse-core/scripts/lantern/minify-trace.js
index c41f5d3c9..4f930158d 100755
--- a/lighthouse-core/scripts/lantern/minify-trace.js
+++ b/lighthouse-core/scripts/lantern/minify-trace.js
@@ -8,17 +8,9 @@
 
 /* eslint-disable no-console */
 
-/**
- * @fileoverview Minifies a trace by removing unnecessary events, throttling screenshots, etc.
- *  See the following files for necessary events:
- *    - lighthouse-core/computed/trace-of-tab.js
- *    - lighthouse-core/computed/page-dependency-graph.js
- *    - lighthouse-core/lib/traces/tracing-processor.js
- */
-
 const fs = require('fs');
 const path = require('path');
-const TracingProcessor = require('../../lib/traces/tracing-processor');
+const {minifyTrace} = require('../../lib/minify-trace.js');
 
 if (process.argv.length !== 4) {
   console.error('Usage $0: <input file> <output file>');
@@ -31,129 +23,16 @@ const inputTraceRaw = fs.readFileSync(inputTracePath, 'utf8');
 /** @type {LH.Trace} */
 const inputTrace = JSON.parse(inputTraceRaw);
 
-const toplevelTaskNames = new Set([
-  'TaskQueueManager::ProcessTaskFromWorkQueue',
-  'ThreadControllerImpl::DoWork',
-  'ThreadControllerImpl::RunTask',
-]);
-
-const traceEventsToAlwaysKeep = new Set([
-  'Screenshot',
-  'TracingStartedInBrowser',
-  'TracingStartedInPage',
-  'navigationStart',
-  'ParseAuthorStyleSheet',
-  'ParseHTML',
-  'PlatformResourceSendRequest',
-  'ResourceSendRequest',
-  'ResourceReceiveResponse',
-  'ResourceFinish',
-  'ResourceReceivedData',
-  'EventDispatch',
-]);
-
-const traceEventsToKeepInToplevelTask = new Set([
-  'TimerInstall',
-  'TimerFire',
-  'InvalidateLayout',
-  'ScheduleStyleRecalculation',
-  'EvaluateScript',
-  'XHRReadyStateChange',
-  'FunctionCall',
-  'v8.compile',
-]);
-
-const traceEventsToKeepInProcess = new Set([
-  ...toplevelTaskNames,
-  ...traceEventsToKeepInToplevelTask,
-  'firstPaint',
-  'firstContentfulPaint',
-  'firstMeaningfulPaint',
-  'firstMeaningfulPaintCandidate',
-  'loadEventEnd',
-  'domContentLoadedEventEnd',
-]);
-
-/**
- * @param {LH.TraceEvent[]} events
- */
-function filterOutUnnecessaryTasksByNameAndDuration(events) {
-  const {pid} = TracingProcessor.findMainFrameIds(events);
-
-  return events.filter(evt => {
-    if (toplevelTaskNames.has(evt.name) && evt.dur < 1000) return false;
-    if (evt.pid === pid && traceEventsToKeepInProcess.has(evt.name)) return true;
-    return traceEventsToAlwaysKeep.has(evt.name);
-  });
-}
-
-/**
- * Filters out tasks that are not within a toplevel task.
- * @param {LH.TraceEvent[]} events
- */
-function filterOutOrphanedTasks(events) {
-  const toplevelRanges = events
-    .filter(evt => toplevelTaskNames.has(evt.name))
-    .map(evt => [evt.ts, evt.ts + evt.dur]);
-
-  /** @param {LH.TraceEvent} e */
-  const isInToplevelTask = e => toplevelRanges.some(([start, end]) => e.ts >= start && e.ts <= end);
-
-  return events.filter((evt, index) => {
-    if (!traceEventsToKeepInToplevelTask.has(evt.name)) return true;
-    if (!isInToplevelTask(evt)) return false;
-
-    if (evt.ph === 'B') {
-      const endEvent = events.slice(index).find(e => e.name === evt.name && e.ph === 'E');
-      return endEvent && isInToplevelTask(endEvent);
-    } else {
-      return true;
-    }
-  });
-}
-
-/**
- * Throttles screenshot events in the trace to 2fps.
- * @param {LH.TraceEvent[]} events
- */
-function filterOutExcessiveScreenshots(events) {
-  const screenshotTimestamps = events.filter(evt => evt.name === 'Screenshot').map(evt => evt.ts);
-
-  let lastScreenshotTs = -Infinity;
-  return events.filter(evt => {
-    if (evt.name !== 'Screenshot') return true;
-    const timeSinceLastScreenshot = evt.ts - lastScreenshotTs;
-    const nextScreenshotTs = screenshotTimestamps.find(ts => ts > evt.ts);
-    const timeUntilNextScreenshot = nextScreenshotTs ? nextScreenshotTs - evt.ts : Infinity;
-    const threshold = 500 * 1000; // Throttle to ~2fps
-    // Keep the frame if it's been more than 500ms since the last frame we kept or the next frame won't happen for at least 500ms
-    const shouldKeep = timeUntilNextScreenshot > threshold || timeSinceLastScreenshot > threshold;
-    if (shouldKeep) lastScreenshotTs = evt.ts;
-    return shouldKeep;
-  });
-}
-
-/**
- * @param {LH.TraceEvent[]} events
- */
-function filterTraceEvents(events) {
-  // Filter out event names we don't care about and tasks <1ms
-  let filtered = filterOutUnnecessaryTasksByNameAndDuration(events);
-  // Filter out events not inside a toplevel task
-  filtered = filterOutOrphanedTasks(filtered);
-  // Filter down the screenshots to key moments + 2fps animations
-  return filterOutExcessiveScreenshots(filtered);
-}
-
-const filteredEvents = filterTraceEvents(inputTrace.traceEvents);
+const outputTrace = minifyTrace(inputTrace);
 const output = `{
   "traceEvents": [
-${filteredEvents.map(e => '    ' + JSON.stringify(e)).join(',\n')}
+${outputTrace.traceEvents.map(e => '    ' + JSON.stringify(e)).join(',\n')}
   ]
 }`;
 
 /** @param {string} s */
 const size = s => Math.round(s.length / 1024) + 'kb';
+const eventDelta = inputTrace.traceEvents.length - outputTrace.traceEvents.length;
 console.log(`Reduced trace from ${size(inputTraceRaw)} to ${size(output)}`);
-console.log(`Filtered out ${inputTrace.traceEvents.length - filteredEvents.length} trace events`);
+console.log(`Filtered out ${eventDelta} trace events`);
 fs.writeFileSync(outputTracePath, output);
diff --git a/lighthouse-core/scripts/roll-to-devtools.sh b/lighthouse-core/scripts/roll-to-devtools.sh
index 2e42c2cc3..fd1068566 100755
--- a/lighthouse-core/scripts/roll-to-devtools.sh
+++ b/lighthouse-core/scripts/roll-to-devtools.sh
@@ -21,6 +21,8 @@ else
   frontend_dir="$chromium_dir/third_party/blink/renderer/devtools/front_end"
 fi
 
+tests_dir="$frontend_dir/../../../web_tests/http/tests/devtools/audits2"
+
 if [[ ! -d "$frontend_dir" || ! -a "$frontend_dir/Runtime.js" ]]; then
   echo -e "\033[31m✖ Error!\033[39m"
   echo "This script requires a devtools frontend folder. We didn't find one here:"
@@ -42,3 +44,9 @@ echo -e "\033[96m ✓\033[39m (Potentially stale) lighthouse-dt-bundle copied."
 
 # copy report generator + cached resources into $fe_lh_dir
 cp -r dist/dt-report-resources/ $fe_lh_dir
+
+# update expected version string in tests
+VERSION=$(node -e "console.log(require('./package.json').version)")
+sed -i '' -e "s/Version:.*/Version: $VERSION/g" "$tests_dir"/*-expected.txt
+
+echo "Done. To rebase the test expectations, run: ~/chromium/src/third_party/blink/tools/run_web_tests.py --no-retry 'http/tests/devtools/audits2/*' --reset-results"
diff --git a/proto/sample_v2_round_trip.json b/proto/sample_v2_round_trip.json
index 3ba37e774..0196d5311 100644
--- a/proto/sample_v2_round_trip.json
+++ b/proto/sample_v2_round_trip.json
@@ -199,6 +199,7 @@
                     {
                         "node": {
                             "explanation": "Fix any of the following:\n  Element has insufficient color contrast of 1.32 (foreground color: #ffc0cb, background color: #eeeeee, font size: 18.0pt, font weight: bold). Expected contrast ratio of 3:1",
+                            "nodeLabel": "Do better web tester page",
                             "path": "3,HTML,1,BODY,5,DIV,0,H2",
                             "selector": "h2",
                             "snippet": "<h2>Do better web tester page</h2>",
@@ -208,6 +209,7 @@
                     {
                         "node": {
                             "explanation": "Fix any of the following:\n  Element has insufficient color contrast of 1.32 (foreground color: #ffc0cb, background color: #eeeeee, font size: 12.0pt, font weight: normal). Expected contrast ratio of 4.5:1",
+                            "nodeLabel": "Hi there!",
                             "path": "3,HTML,1,BODY,5,DIV,1,SPAN",
                             "selector": "span",
                             "snippet": "<span>Hi there!</span>",
@@ -871,6 +873,7 @@
                     {
                         "node": {
                             "explanation": "Fix any of the following:\n  The <html> element does not have a lang attribute",
+                            "nodeLabel": "html",
                             "path": "3,HTML",
                             "selector": "html",
                             "snippet": "<html manifest=\"clock.appcache\">",
@@ -924,6 +927,7 @@
                     {
                         "node": {
                             "explanation": "Fix any of the following:\n  Element does not have an alt attribute\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\"",
+                            "nodeLabel": "img",
                             "path": "3,HTML,1,BODY,10,IMG",
                             "selector": "img[height=\"\\35 7\"]",
                             "snippet": "<img src=\"lighthouse-480x318.jpg\" width=\"480\" height=\"57\">",
@@ -933,6 +937,7 @@
                     {
                         "node": {
                             "explanation": "Fix any of the following:\n  Element does not have an alt attribute\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\"",
+                            "nodeLabel": "img",
                             "path": "3,HTML,1,BODY,12,IMG",
                             "selector": "img[height=\"\\33 18\"]",
                             "snippet": "<img src=\"lighthouse-480x318.jpg\" width=\"480\" height=\"318\">",
@@ -942,8 +947,9 @@
                     {
                         "node": {
                             "explanation": "Fix any of the following:\n  Element does not have an alt attribute\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\"",
+                            "nodeLabel": "img",
                             "path": "3,HTML,1,BODY,14,IMG",
-                            "selector": "img[src$=\"lighthouse-rotating.gif\"]",
+                            "selector": "img[src$=\"lighthouse-rotating\\.gif\"]",
                             "snippet": "<img src=\"lighthouse-rotating.gif\" width=\"811\" height=\"462\">",
                             "type": "node"
                         }
@@ -951,8 +957,9 @@
                     {
                         "node": {
                             "explanation": "Fix any of the following:\n  Element does not have an alt attribute\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\"",
-                            "path": "3,HTML,1,BODY,49,IMG",
-                            "selector": "img:nth-child(27)",
+                            "nodeLabel": "img",
+                            "path": "3,HTML,1,BODY,53,IMG",
+                            "selector": "img:nth-child(30)",
                             "snippet": "<img src=\"blob:http://localhost:62824/289254fd-ef1d-4c1a-96a8-ba291caa2140\">",
                             "type": "node"
                         }
@@ -1142,7 +1149,8 @@
                     {
                         "node": {
                             "explanation": "Fix any of the following:\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Form element does not have an implicit (wrapped) <label>\n  Form element does not have an explicit <label>\n  Element has no title attribute or the title attribute is empty",
-                            "path": "3,HTML,1,BODY,44,INPUT",
+                            "nodeLabel": "input",
+                            "path": "3,HTML,1,BODY,48,INPUT",
                             "selector": "input[onpaste=\"event\\.preventDefault\\(\\)\\;\"]",
                             "snippet": "<input type=\"password\" onpaste=\"event.preventDefault();\">",
                             "type": "node"
@@ -1151,8 +1159,9 @@
                     {
                         "node": {
                             "explanation": "Fix any of the following:\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Form element does not have an implicit (wrapped) <label>\n  Form element does not have an explicit <label>\n  Element has no title attribute or the title attribute is empty",
-                            "path": "3,HTML,1,BODY,46,INPUT",
-                            "selector": "input:nth-child(25)",
+                            "nodeLabel": "input",
+                            "path": "3,HTML,1,BODY,50,INPUT",
+                            "selector": "input:nth-child(28)",
                             "snippet": "<input type=\"password\">",
                             "type": "node"
                         }
@@ -1160,7 +1169,8 @@
                     {
                         "node": {
                             "explanation": "Fix any of the following:\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Form element does not have an implicit (wrapped) <label>\n  Form element does not have an explicit <label>\n  Element has no title attribute or the title attribute is empty",
-                            "path": "3,HTML,1,BODY,48,INPUT",
+                            "nodeLabel": "input",
+                            "path": "3,HTML,1,BODY,52,INPUT",
                             "selector": "input[onpaste=\"return\\ false\\;\"]",
                             "snippet": "<input type=\"password\" onpaste=\"return false;\">",
                             "type": "node"
@@ -1207,8 +1217,9 @@
                     {
                         "node": {
                             "explanation": "Fix all of the following:\n  Element is in tab order and does not have accessible text\n\nFix any of the following:\n  Element does not have text that is visible to screen readers\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\"",
-                            "path": "3,HTML,1,BODY,40,A",
-                            "selector": "a[href=\"javascript:void(0)\"]",
+                            "nodeLabel": "a",
+                            "path": "3,HTML,1,BODY,44,A",
+                            "selector": "a[href=\"javascript\\:void\\(0\\)\"]",
                             "snippet": "<a href=\"javascript:void(0)\" target=\"_blank\"></a>",
                             "type": "node"
                         }
@@ -1216,8 +1227,9 @@
                     {
                         "node": {
                             "explanation": "Fix all of the following:\n  Element is in tab order and does not have accessible text\n\nFix any of the following:\n  Element does not have text that is visible to screen readers\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\"",
-                            "path": "3,HTML,1,BODY,42,A",
-                            "selector": "a[href$=\"mailto:inbox@email.com\"]",
+                            "nodeLabel": "a",
+                            "path": "3,HTML,1,BODY,46,A",
+                            "selector": "a[href$=\"mailto\\:inbox\\@email\\.com\"]",
                             "snippet": "<a href=\"mailto:inbox@email.com\" target=\"_blank\"></a>",
                             "type": "node"
                         }
@@ -1952,7 +1964,8 @@
                 "items": [
                     {
                         "node": {
-                            "explanation": "Fix any of the following:\n  Element does not have text that is visible to screen readers\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty",
+                            "explanation": "Fix any of the following:\n  Element does not have text that is visible to screen readers\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\"",
+                            "nodeLabel": "object",
                             "path": "3,HTML,1,BODY,2,OBJECT",
                             "selector": "#\\35 934a",
                             "snippet": "<object id=\"5934a\"></object>",
@@ -1961,7 +1974,8 @@
                     },
                     {
                         "node": {
-                            "explanation": "Fix any of the following:\n  Element does not have text that is visible to screen readers\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty",
+                            "explanation": "Fix any of the following:\n  Element does not have text that is visible to screen readers\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\"",
+                            "nodeLabel": "object",
                             "path": "3,HTML,1,BODY,3,OBJECT",
                             "selector": "#\\35 934b",
                             "snippet": "<object id=\"5934b\"></object>",
@@ -2481,6 +2495,7 @@
                         "height": 18.0,
                         "overlapScoreRatio": 0.8333333333333334,
                         "overlappingTarget": {
+                            "nodeLabel": "Do something else",
                             "path": "3,HTML,1,BODY,18,BUTTON",
                             "selector": "body > button.small-button",
                             "snippet": "<button class=\"small-button\">Do something else</button>",
@@ -2489,6 +2504,7 @@
                         "overlappingTargetScore": 720.0,
                         "size": "200x18",
                         "tapTarget": {
+                            "nodeLabel": "Do something",
                             "path": "3,HTML,1,BODY,17,BUTTON",
                             "selector": "body > button.small-button",
                             "snippet": "<button class=\"small-button\">Do something</button>",
diff --git a/readme.md b/readme.md
index 5c57b59d2..f147272b8 100644
--- a/readme.md
+++ b/readme.md
@@ -316,7 +316,7 @@ Other awesome open source projects that use Lighthouse.
 * **[lighthouse4u](https://github.com/godaddy/lighthouse4u)** - LH4U provides Google Lighthouse as a service, surfaced by both a friendly UI+API, and backed by Elastic Search for easy querying and visualization.
 * **[lighthouse-gh-reporter](https://github.com/carlesnunez/lighthouse-gh-reporter)** - Run Lighthouse in CI and report back in a comment on your pull requests
 * **[react-lighthouse-viewer](https://www.npmjs.com/package/react-lighthouse-viewer)** - Render a Lighthouse JSON report in a React Component.
-
+* **[performance-budgets](https://performance-budgets.netlify.com/)** - Easily assert Lighthouse budgets with Docker.
 ## FAQ
 
 ### How does Lighthouse work?
diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts
index f28e7b1dc..860a0520f 100644
--- a/types/artifacts.d.ts
+++ b/types/artifacts.d.ts
@@ -145,6 +145,7 @@ declare global {
             snippet: string;
             target: string[];
             failureSummary?: string;
+            nodeLabel?: string;
           }[];
         }[];
         notApplicable: {
@@ -337,11 +338,12 @@ declare global {
       }
 
       export interface TapTarget {
-        snippet: string,
-        selector: string,
-        path: string,
-        href: string,
-        clientRects: Rect[]
+        snippet: string;
+        selector: string;
+        nodeLabel?: string;
+        path: string;
+        href: string;
+        clientRects: Rect[];
       }
 
       export interface ViewportDimensions {
diff --git a/types/audit-details.d.ts b/types/audit-details.d.ts
index ca670c89a..155d52ae7 100644
--- a/types/audit-details.d.ts
+++ b/types/audit-details.d.ts
@@ -160,7 +160,10 @@ declare global {
         type: 'node';
         path?: string;
         selector?: string;
+        /** An HTML snippet used to identify the node. */
         snippet?: string;
+        /** A human-friendly text descriptor that's used to identify the node more quickly. */
+        nodeLabel?: string;
       }
 
       /**
diff --git a/types/budget.d.ts b/types/budget.d.ts
index fa8632f1b..4c1d058b2 100644
--- a/types/budget.d.ts
+++ b/types/budget.d.ts
@@ -37,7 +37,7 @@ declare global {
       }
 
       /** Supported timing metrics. */
-      export type TimingMetric = 'first-contentful-paint' | 'first-cpu-idle' | 'interactive' | 'first-meaningful-paint' | 'estimated-input-latency';
+      export type TimingMetric = 'first-contentful-paint' | 'first-cpu-idle' | 'interactive' | 'first-meaningful-paint' | 'max-potential-fid';
 
       /** Supported values for the resourceType property of a ResourceBudget. */
       export type ResourceType = 'stylesheet' | 'image' | 'media' | 'font' | 'script' | 'document' | 'other' | 'total' | 'third-party';


Test patch:
diff --git a/lighthouse-cli/test/smokehouse/a11y/expectations.js b/lighthouse-cli/test/smokehouse/a11y/expectations.js
index bc3e29fa3..9f29e47c3 100644
--- a/lighthouse-cli/test/smokehouse/a11y/expectations.js
+++ b/lighthouse-cli/test/smokehouse/a11y/expectations.js
@@ -5,6 +5,8 @@
  */
 'use strict';
 
+/* eslint-disable max-len */
+
 /**
  * Expected Lighthouse audit values for byte efficiency tests
  */
@@ -17,225 +19,442 @@ module.exports = [
         'aria-allowed-attr': {
           score: 0,
           details: {
-            items: {
-              length: 1,
-            },
+            items: [
+              {
+                node: {
+                  'type': 'node',
+                  'selector': '#aria-allowed-attr',
+                  'snippet': '<div id="aria-allowed-attr" role="alert" aria-checked="true">\n      </div>',
+                  'explanation': 'Fix any of the following:\n  ARIA attribute is not allowed: aria-checked="true"',
+                  'nodeLabel': 'div',
+                },
+              },
+            ],
           },
         },
         'aria-required-children': {
           score: 0,
           details: {
-            items: {
-              length: 1,
-            },
+            items: [
+              {
+                node: {
+                  'type': 'node',
+                  'selector': '#aria-required-children',
+                  'snippet': '<div id="aria-required-children" role="radiogroup">\n        <div></div>\n      </div>',
+                  'explanation': 'Fix any of the following:\n  Required ARIA child role not present: radio',
+                  'nodeLabel': 'div',
+                },
+              },
+            ],
           },
         },
         'aria-required-parent': {
           score: 0,
           details: {
-            items: {
-              length: 1,
-            },
+            items: [
+              {
+                node: {
+                  'type': 'node',
+                  'selector': '#aria-required-parent',
+                  'snippet': '<div id="aria-required-parent" role="option">\n        </div>',
+                  'explanation': 'Fix any of the following:\n  Required ARIA parent role not present: listbox',
+                  'nodeLabel': 'div',
+                },
+              },
+            ],
           },
         },
         'aria-roles': {
           score: 0,
           details: {
-            items: {
-              length: 1,
-            },
+            items: [
+              {
+                node: {
+                  'type': 'node',
+                  'selector': 'div[role="foo"]',
+                  'snippet': '<div role="foo"></div>',
+                  'explanation': 'Fix all of the following:\n  Role must be one of the valid ARIA roles',
+                  'nodeLabel': 'div',
+                },
+              },
+            ],
           },
         },
         'aria-valid-attr-value': {
           score: 0,
           details: {
-            items: {
-              length: 1,
-            },
+            items: [
+              {
+                node: {
+                  'type': 'node',
+                  'selector': '#aria-valid-attr-value',
+                  'snippet': '<div id="aria-valid-attr-value" role="checkbox" aria-checked="0">\n      </div>',
+                  'explanation': 'Fix all of the following:\n  Invalid ARIA attribute value: aria-checked="0"',
+                  'nodeLabel': 'div',
+                },
+              },
+            ],
           },
         },
         'aria-valid-attr': {
           score: 0,
           details: {
-            items: {
-              length: 1,
-            },
+            items: [
+              {
+                node: {
+                  'type': 'node',
+                  'selector': '#aria-valid-attr',
+                  'snippet': '<div id="aria-valid-attr" role="checkbox" aria-chked="true">\n      </div>',
+                  'explanation': 'Fix any of the following:\n  Invalid ARIA attribute name: aria-chked',
+                  'nodeLabel': 'div',
+                },
+              },
+            ],
           },
         },
         'button-name': {
           score: 0,
           details: {
-            items: {
-              length: 1,
-            },
+            items: [
+              {
+                node: {
+                  'type': 'node',
+                  'selector': '#button-name',
+                  'snippet': '<button id="button-name"></button>',
+                  'explanation': 'Fix all of the following:\n  Element is in tab order and does not have accessible text\n\nFix any of the following:\n  Element has a value attribute and the value attribute is empty\n  Element has no value attribute or the value attribute is empty\n  Element does not have inner text that is visible to screen readers\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element\'s default semantics were not overridden with role="presentation"\n  Element\'s default semantics were not overridden with role="none"\n  Element has no title attribute or the title attribute is empty',
+                  'nodeLabel': 'button',
+                },
+              },
+            ],
           },
         },
         'bypass': {
           score: 0,
           details: {
-            items: {
-              length: 1,
-            },
+            items: [
+              {
+                node: {
+                  'type': 'node',
+                  'selector': 'html',
+                  'snippet': '<html>',
+                  'explanation': 'Fix any of the following:\n  No valid skip link found\n  Page does not have a header\n  Page does not have a landmark region',
+                  'nodeLabel': 'html',
+                },
+              },
+            ],
           },
         },
         'color-contrast': {
           score: 0,
           details: {
-            items: {
-              length: 1,
-            },
+            items: [
+              {
+                node: {
+                  'type': 'node',
+                  'selector': '#color-contrast',
+                  'snippet': '<div id="color-contrast" style="background-color: red; color: pink;">\n          Hello\n      </div>',
+                  'explanation': 'Fix any of the following:\n  Element has insufficient color contrast of 2.59 (foreground color: #ffc0cb, background color: #ff0000, font size: 28.5pt, font weight: normal). Expected contrast ratio of 3:1',
+                  'nodeLabel': 'Hello',
+                },
+              },
+            ],
           },
         },
         'definition-list': {
           score: 0,
           details: {
-            items: {
-              length: 1,
-            },
+            items: [
+              {
+                node: {
+                  'type': 'node',
+                  'selector': '#definition-list',
+                  'snippet': '<dl id="definition-list">\n        <p></p>\n      </dl>',
+                  'explanation': 'Fix all of the following:\n  List element has direct children that are not allowed inside <dt> or <dd> elements',
+                  'nodeLabel': 'dl',
+                },
+              },
+            ],
           },
         },
         'dlitem': {
           score: 0,
           details: {
-            items: {
-              length: 1,
-            },
+            items: [
+              {
+                node: {
+                  'type': 'node',
+                  'selector': 'dd',
+                  'snippet': '<dd></dd>',
+                  'explanation': 'Fix any of the following:\n  Description list item does not have a <dl> parent element',
+                  'nodeLabel': 'dd',
+                },
+              },
+            ],
           },
         },
         'document-title': {
           score: 0,
           details: {
-            items: {
-              length: 1,
-            },
+            items: [
+              {
+                node: {
+                  'type': 'node',
+                  'selector': 'html',
+                  'snippet': '<html>',
+                  'explanation': 'Fix any of the following:\n  Document does not have a non-empty <title> element',
+                  'nodeLabel': 'html',
+                },
+              },
+            ],
+
           },
         },
         'duplicate-id': {
           score: 0,
           details: {
-            items: {
-              length: 1,
-            },
+            items: [
+              {
+                node: {
+                  'type': 'node',
+                  'selector': 'section:nth-child(28) > div:nth-child(1)',
+                  'snippet': '<div id="duplicate-id"></div>',
+                  'explanation': 'Fix any of the following:\n  Document has multiple static elements with the same id attribute',
+                  'nodeLabel': 'div',
+                },
+              },
+            ],
           },
         },
         'frame-title': {
           score: 0,
           details: {
-            items: {
-              length: 1,
-            },
+            items: [
+              {
+                node: {
+                  'type': 'node',
+                  'selector': '#frame-title',
+                  'snippet': '<iframe id="frame-title"></iframe>',
+                  'explanation': 'Fix any of the following:\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty\n  Element\'s default semantics were not overridden with role="presentation"\n  Element\'s default semantics were not overridden with role="none"',
+                  'nodeLabel': 'iframe',
+                },
+              },
+            ],
           },
         },
         'html-has-lang': {
           score: 0,
           details: {
-            items: {
-              length: 1,
-            },
+            items: [
+              {
+                node: {
+                  'type': 'node',
+                  'selector': 'html',
+                  'snippet': '<html>',
+                  'explanation': 'Fix any of the following:\n  The <html> element does not have a lang attribute',
+                  'nodeLabel': 'html',
+                },
+              },
+            ],
           },
         },
         'image-alt': {
           score: 0,
           details: {
-            items: {
-              length: 1,
-            },
+            items: [
+              {
+                node: {
+                  'type': 'node',
+                  'selector': '#image-alt',
+                  'snippet': '<img id="image-alt" src="./bogus.jpg">',
+                  'explanation': 'Fix any of the following:\n  Element does not have an alt attribute\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty\n  Element\'s default semantics were not overridden with role="presentation"\n  Element\'s default semantics were not overridden with role="none"',
+                  'nodeLabel': 'img',
+                },
+              },
+            ],
           },
         },
         'input-image-alt': {
           score: 0,
           details: {
-            items: {
-              length: 1,
-            },
+            items: [
+              {
+                node: {
+                  'type': 'node',
+                  'selector': '#input-image-alt',
+                  'snippet': '<input type="image" id="input-image-alt">',
+                  'explanation': 'Fix any of the following:\n  Element has no alt attribute or the alt attribute is empty\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty',
+                  'nodeLabel': 'input',
+                },
+              },
+            ],
           },
         },
         'label': {
           score: 0,
           details: {
-            items: {
-              length: 1,
-            },
+            items: [
+              {
+                node: {
+                  'type': 'node',
+                  'selector': '#label',
+                  'snippet': '<input id="label" type="text">',
+                  'explanation': 'Fix any of the following:\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Form element does not have an implicit (wrapped) <label>\n  Form element does not have an explicit <label>\n  Element has no title attribute or the title attribute is empty',
+                  'nodeLabel': 'input',
+                },
+              },
+            ],
           },
         },
         'layout-table': {
           score: 1,
           details: {
-            items: {
-              length: 0,
-            },
+            'type': 'table',
+            'headings': [],
+            'items': [],
           },
         },
         'link-name': {
           score: 0,
           details: {
-            items: {
-              length: 1,
-            },
+            items: [
+              {
+                node: {
+                  'type': 'node',
+                  'selector': '#link-name',
+                  'snippet': '<a id="link-name" href="google.com"></a>',
+                  'explanation': 'Fix all of the following:\n  Element is in tab order and does not have accessible text\n\nFix any of the following:\n  Element does not have text that is visible to screen readers\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element\'s default semantics were not overridden with role="presentation"\n  Element\'s default semantics were not overridden with role="none"',
+                  'nodeLabel': 'a',
+                },
+              },
+            ],
           },
         },
         'list': {
           score: 0,
           details: {
-            items: {
-              length: 1,
-            },
+            items: [
+              {
+                node: {
+                  'type': 'node',
+                  'selector': '#list',
+                  'snippet': '<ul id="list">\n        <p></p>\n      </ul>',
+                  'explanation': 'Fix all of the following:\n  List element has direct children that are not allowed inside <li> elements',
+                  'nodeLabel': 'ul',
+                },
+              },
+            ],
           },
         },
         'listitem': {
           score: 0,
           details: {
-            items: {
-              length: 1,
-            },
+            items: [
+              {
+                node: {
+                  'type': 'node',
+                  'selector': '#listitem',
+                  'snippet': '<li id="listitem"></li>',
+                  'explanation': 'Fix any of the following:\n  List item does not have a <ul>, <ol> or role="list" parent element',
+                  'nodeLabel': 'li',
+                },
+              },
+            ],
           },
         },
         'meta-viewport': {
           score: 0,
           details: {
-            items: {
-              length: 1,
-            },
+            items: [
+              {
+                node: {
+                  'type': 'node',
+                  'selector': 'meta[name="viewport"]',
+                  'snippet': '<meta name="viewport" content="user-scalable=no, maximum-scale=1.0">',
+                  'explanation': 'Fix any of the following:\n  user-scalable=no on <meta> tag disables zooming on mobile devices',
+                  'nodeLabel': 'meta',
+                },
+              },
+            ],
           },
         },
         'object-alt': {
           score: 0,
           details: {
-            items: {
-              length: 1,
-            },
+            items: [
+              {
+                node: {
+                  'type': 'node',
+                  'selector': '#object-alt',
+                  'snippet': '<object id="object-alt"></object>',
+                  'explanation': 'Fix any of the following:\n  Element does not have text that is visible to screen readers\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty\n  Element\'s default semantics were not overridden with role="presentation"\n  Element\'s default semantics were not overridden with role="none"',
+                  'nodeLabel': 'object',
+                },
+              },
+            ],
           },
         },
         'tabindex': {
           score: 0,
           details: {
-            items: {
-              length: 1,
-            },
+            items: [
+              {
+                node: {
+                  'type': 'node',
+                  'selector': '#tabindex',
+                  'snippet': '<div id="tabindex" tabindex="10">\n      </div>',
+                  'explanation': 'Fix any of the following:\n  Element has a tabindex greater than 0',
+                  'nodeLabel': 'div',
+                },
+              },
+            ],
           },
         },
         'td-headers-attr': {
           score: 0,
           details: {
-            items: {
-              length: 1,
-            },
+            items: [
+              {
+                node: {
+                  'type': 'node',
+                  'selector': '#td-headers-attr',
+                  'snippet': '<table id="td-headers-attr">\n\t\t\t  <tbody><tr><th>FOO</th></tr>\n\t\t\t  <tr><td headers="bogus-td-headers-attr">foo</td></tr>\n\t\t\t</tbody></table>',
+                  'explanation': 'Fix all of the following:\n  The headers attribute is not exclusively used to refer to other cells in the table',
+                  'nodeLabel': 'FOO\nfoo',
+                },
+              },
+            ],
           },
         },
         'valid-lang': {
           score: 0,
           details: {
-            items: {
-              length: 1,
-            },
+            items: [
+              {
+                node: {
+                  'type': 'node',
+                  'selector': '#valid-lang',
+                  'snippet': '<p id="valid-lang" lang="foo">foo</p>',
+                  'explanation': 'Fix all of the following:\n  Value of lang attribute not included in the list of valid languages',
+                  'nodeLabel': 'foo',
+                },
+              },
+            ],
           },
         },
         'accesskeys': {
           score: 0,
           details: {
-            items: {
-              length: 1,
-            },
+            items: [
+              {
+                node: {
+                  'type': 'node',
+                  'selector': '#accesskeys1',
+                  'snippet': '<button id="accesskeys1" accesskey="s">Foo</button>',
+                  'explanation': 'Fix all of the following:\n  Document has multiple elements with the same accesskey',
+                  'nodeLabel': 'Foo',
+                },
+              },
+            ],
           },
         },
       },
diff --git a/lighthouse-cli/test/smokehouse/perf/perf-config.js b/lighthouse-cli/test/smokehouse/perf/perf-config.js
index 6b22341a9..f253a0502 100644
--- a/lighthouse-cli/test/smokehouse/perf/perf-config.js
+++ b/lighthouse-cli/test/smokehouse/perf/perf-config.js
@@ -41,7 +41,7 @@ const perfConfig = {
         {metric: 'first-cpu-idle', budget: 2000, tolerance: 100},
         {metric: 'interactive', budget: 2000, tolerance: 100},
         {metric: 'first-meaningful-paint', budget: 2000, tolerance: 100},
-        {metric: 'estimated-input-latency', budget: 2000, tolerance: 100},
+        {metric: 'max-potential-fid', budget: 2000, tolerance: 100},
       ],
     }],
   },
diff --git a/lighthouse-cli/test/smokehouse/seo/expectations.js b/lighthouse-cli/test/smokehouse/seo/expectations.js
index 4057997f1..8b8e5f71a 100644
--- a/lighthouse-cli/test/smokehouse/seo/expectations.js
+++ b/lighthouse-cli/test/smokehouse/seo/expectations.js
@@ -205,6 +205,7 @@ module.exports = [
                  '\n        too small target\n      </a>',
                   'path': '2,HTML,1,BODY,3,DIV,21,DIV,0,A',
                   'selector': 'body > div > div > a',
+                  'nodeLabel': 'too small target',
                 },
                 'overlappingTarget': {
                   'type': 'node',
@@ -213,6 +214,7 @@ module.exports = [
                   '\n        big enough target\n      </a>',
                   'path': '2,HTML,1,BODY,3,DIV,21,DIV,1,A',
                   'selector': 'body > div > div > a',
+                  'nodeLabel': 'big enough target',
                 },
                 'size': '100x30',
                 'width': 100,
diff --git a/lighthouse-core/test/config/budget-test.js b/lighthouse-core/test/config/budget-test.js
index 19de4fb00..20b4843e8 100644
--- a/lighthouse-core/test/config/budget-test.js
+++ b/lighthouse-core/test/config/budget-test.js
@@ -146,6 +146,18 @@ describe('Budget', () => {
       assert.throws(_ => Budget.initializeBudget(budget),
         /Resource Budget has unrecognized properties: \[browser\]/);
     });
+
+    it('throws when a duplicate resourceType is specified in resourceSizes', () => {
+      budget[1].resourceSizes.push({resourceType: 'script', budget: 100});
+      assert.throws(_ => Budget.initializeBudget(budget),
+        /budgets\[1\]\.resourceSizes has duplicate entry of type 'script'/);
+    });
+
+    it('throws when a duplicate resourceType is specified in resourceCounts', () => {
+      budget[0].resourceCounts.push({resourceType: 'third-party', budget: 100});
+      assert.throws(_ => Budget.initializeBudget(budget),
+        /budgets\[0\]\.resourceCounts has duplicate entry of type 'third-party'/);
+    });
   });
 
   describe('timing budget validation', () => {
@@ -172,5 +184,11 @@ describe('Budget', () => {
       assert.throws(_ => Budget.initializeBudget(budget),
         /Timing Budget has unrecognized properties: \[device, location\]/);
     });
+
+    it('throws when a duplicate metric type is specified in timings', () => {
+      budget[0].timings.push({metric: 'interactive', budget: 1000});
+      assert.throws(_ => Budget.initializeBudget(budget),
+        /budgets\[0\]\.timings has duplicate entry of type 'interactive'/);
+    });
   });
 });
diff --git a/lighthouse-core/test/lib/minify-devtoolslog-test.js b/lighthouse-core/test/lib/minify-devtoolslog-test.js
new file mode 100644
index 000000000..dbd2f3a6a
--- /dev/null
+++ b/lighthouse-core/test/lib/minify-devtoolslog-test.js
@@ -0,0 +1,33 @@
+/**
+ * @license Copyright 2019 Google Inc. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+'use strict';
+
+const {minifyDevtoolsLog} = require('../../lib/minify-devtoolslog.js');
+const trace = require('../fixtures/traces/progressive-app-m60.json');
+const devtoolsLog = require('../fixtures/traces/progressive-app-m60.devtools.log.json');
+const MetricsAudit = require('../../audits/metrics.js');
+
+/* eslint-env jest */
+
+describe('minify-devtoolslog', () => {
+  it('has identical metrics to unminified', async () => {
+    const artifacts = {traces: {defaultPass: trace}, devtoolsLogs: {defaultPass: devtoolsLog}};
+    const context = {settings: {throttlingMethod: 'simulate'}, computedCache: new Map()};
+    const {details: {items: [before]}} = await MetricsAudit.audit(artifacts, context);
+    const beforeSize = JSON.stringify(devtoolsLog).length;
+
+    const minifiedDevtoolsLog = minifyDevtoolsLog(devtoolsLog);
+    artifacts.devtoolsLogs.defaultPass = minifiedDevtoolsLog;
+    context.computedCache.clear(); // not strictly necessary, but we'll be safe
+    const {details: {items: [after]}} = await MetricsAudit.audit(artifacts, context);
+    const afterSize = JSON.stringify(minifiedDevtoolsLog).length;
+
+    // It should reduce the size of the log.
+    expect(afterSize).toBeLessThan(beforeSize * 0.5);
+    // And not affect the metrics.
+    expect(after).toEqual(before);
+  });
+});
diff --git a/lighthouse-core/test/lib/minify-trace-test.js b/lighthouse-core/test/lib/minify-trace-test.js
new file mode 100644
index 000000000..6bc1f1abf
--- /dev/null
+++ b/lighthouse-core/test/lib/minify-trace-test.js
@@ -0,0 +1,43 @@
+/**
+ * @license Copyright 2019 Google Inc. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+'use strict';
+
+const minifyTrace = require('../../lib/minify-trace.js').minifyTrace;
+const trace = require('../fixtures/traces/progressive-app-m60.json');
+const devtoolsLog = require('../fixtures/traces/progressive-app-m60.devtools.log.json');
+const MetricsAudit = require('../../audits/metrics.js');
+
+/* eslint-env jest */
+
+describe('minify-trace', () => {
+  it('has identical metrics to unminified', async () => {
+    const artifacts = {traces: {defaultPass: trace}, devtoolsLogs: {defaultPass: devtoolsLog}};
+    const context = {settings: {throttlingMethod: 'simulate'}, computedCache: new Map()};
+    const {details: {items: [before]}} = await MetricsAudit.audit(artifacts, context);
+    const beforeSize = JSON.stringify(trace).length;
+
+    const minifiedTrace = minifyTrace(trace);
+    artifacts.traces.defaultPass = minifiedTrace;
+    context.computedCache.clear();
+    const {details: {items: [after]}} = await MetricsAudit.audit(artifacts, context);
+    const afterSize = JSON.stringify(minifiedTrace).length;
+
+    for (const key of Object.keys(after)) {
+      // Speed Index is expected to be different because of screenshot throttling.
+      // Trace End can also be different if the last event was unimportant.
+      // There are many different `observed*` and `*Ts` versions, so remove with a regex.
+      if (/speedIndex|traceEnd/i.test(key)) {
+        delete before[key];
+        delete after[key];
+      }
+    }
+
+    // It should greatly reduce the size of the trace.
+    expect(afterSize).toBeLessThan(beforeSize * 0.2);
+    // And not affect the metrics.
+    expect(after).toEqual(before);
+  });
+});
diff --git a/lighthouse-core/test/lib/page-functions-test.js b/lighthouse-core/test/lib/page-functions-test.js
index d04114a0c..8906eda99 100644
--- a/lighthouse-core/test/lib/page-functions-test.js
+++ b/lighthouse-core/test/lib/page-functions-test.js
@@ -59,4 +59,37 @@ describe('Page Functions', () => {
       assert.equal(pageFunctions.getNodeSelector(childEl), 'div#wrapper > div.child');
     });
   });
+
+  describe('getNodeLabel', () => {
+    it('Returns innerText if element has visible text', () => {
+      const el = dom.createElement('div');
+      el.innerText = 'Hello';
+      assert.equal(pageFunctions.getNodeLabel(el), 'Hello');
+    });
+
+    it('Falls back to children and alt/aria-label if a title can\'t be determined', () => {
+      const el = dom.createElement('div');
+      const childEl = dom.createElement('div', '', {'aria-label': 'Something'});
+      el.appendChild(childEl);
+      assert.equal(pageFunctions.getNodeLabel(el), 'Something');
+    });
+
+    it('Truncates long text', () => {
+      const el = dom.createElement('div');
+      el.setAttribute('alt', Array(100).fill('a').join(''));
+      assert.equal(pageFunctions.getNodeLabel(el).length, 80);
+    });
+
+    it('Uses tag name for html tags', () => {
+      const el = dom.createElement('html');
+      assert.equal(pageFunctions.getNodeLabel(el), 'html');
+    });
+
+    it('Uses tag name if there is no better label', () => {
+      const el = dom.createElement('div');
+      const childEl = dom.createElement('span');
+      el.appendChild(childEl);
+      assert.equal(pageFunctions.getNodeLabel(el), 'div');
+    });
+  });
 });
diff --git a/lighthouse-core/test/report/html/renderer/dom-test.js b/lighthouse-core/test/report/html/renderer/dom-test.js
index ea91f810f..54c623dad 100644
--- a/lighthouse-core/test/report/html/renderer/dom-test.js
+++ b/lighthouse-core/test/report/html/renderer/dom-test.js
@@ -69,8 +69,8 @@ describe('DOM', () => {
     });
 
     it('does not inject duplicate styles', () => {
-      const clone = dom.cloneTemplate('#tmpl-lh-gauge', dom.document());
-      const clone2 = dom.cloneTemplate('#tmpl-lh-gauge', dom.document());
+      const clone = dom.cloneTemplate('#tmpl-lh-snippet', dom.document());
+      const clone2 = dom.cloneTemplate('#tmpl-lh-snippet', dom.document());
       assert.ok(clone.querySelector('style'));
       assert.ok(!clone2.querySelector('style'));
     });
diff --git a/lighthouse-core/test/report/html/renderer/psi-test.js b/lighthouse-core/test/report/html/renderer/psi-test.js
index 57823bcad..c9b0b346d 100644
--- a/lighthouse-core/test/report/html/renderer/psi-test.js
+++ b/lighthouse-core/test/report/html/renderer/psi-test.js
@@ -82,7 +82,6 @@ describe('DOM', () => {
         const result = prepareLabData(sampleResultsStr, document);
         assert.ok(result.scoreGaugeEl instanceof document.defaultView.Element);
         assert.equal(result.scoreGaugeEl.querySelector('.lh-gauge__wrapper').href, '');
-        assert.ok(result.scoreGaugeEl.outerHTML.includes('<style>'), 'score gauge comes with CSS');
         assert.ok(result.scoreGaugeEl.outerHTML.includes('<svg'), 'score gauge comes with SVG');
 
         assert.ok(result.perfCategoryEl instanceof document.defaultView.Element);
diff --git a/lighthouse-core/test/report/html/renderer/report-ui-features-test.js b/lighthouse-core/test/report/html/renderer/report-ui-features-test.js
index 54a3d7852..ca825732d 100644
--- a/lighthouse-core/test/report/html/renderer/report-ui-features-test.js
+++ b/lighthouse-core/test/report/html/renderer/report-ui-features-test.js
@@ -124,6 +124,10 @@ describe('ReportUIFeatures', () => {
         const lhr = JSON.parse(JSON.stringify(sampleResults));
         lhr.requestedUrl = lhr.finalUrl = 'http://www.example.com';
         const webpAuditItemTemplate = sampleResults.audits['uses-webp-images'].details.items[0];
+        const renderBlockingAuditItemTemplate =
+          sampleResults.audits['render-blocking-resources'].details.items[0];
+        const textCompressionAuditItemTemplate =
+          sampleResults.audits['uses-text-compression'].details.items[0];
         // Interleave first/third party URLs to test restoring order.
         lhr.audits['uses-webp-images'].details.items = [
           {
@@ -140,6 +144,38 @@ describe('ReportUIFeatures', () => {
           },
         ];
 
+        // Only third party URLs to test that checkbox is hidden
+        lhr.audits['render-blocking-resources'].details.items = [
+          {
+            ...renderBlockingAuditItemTemplate,
+            url: 'http://www.cdn.com/script1.js', // Third party.
+          },
+          {
+            ...renderBlockingAuditItemTemplate,
+            url: 'http://www.google.com/script2.js', // Third party.
+          },
+          {
+            ...renderBlockingAuditItemTemplate,
+            url: 'http://www.notexample.com/script3.js', // Third party.
+          },
+        ];
+
+        // Only first party URLs to test that checkbox is hidden
+        lhr.audits['uses-text-compression'].details.items = [
+          {
+            ...textCompressionAuditItemTemplate,
+            url: 'http://www.example.com/font1.ttf', // First party.
+          },
+          {
+            ...textCompressionAuditItemTemplate,
+            url: 'http://www.example.com/font2.ttf', // First party.
+          },
+          {
+            ...textCompressionAuditItemTemplate,
+            url: 'http://www.example.com/font3.ttf', // First party.
+          },
+        ];
+
         // render a report onto the UIFeature dom
         container = render(lhr);
       });
@@ -160,7 +196,7 @@ describe('ReportUIFeatures', () => {
         expect(getUrlsInTable()).toEqual(['/img1.jpg', '/img2.jpg', '/img3.jpg']);
       });
 
-      it('adds no filter for audits that do not need them', () => {
+      it('adds no filter for audits in thirdPartyFilterAuditExclusions', () => {
         const checkboxClassName = 'lh-3p-filter-input';
 
         const yesCheckbox = dom.find(`#uses-webp-images .${checkboxClassName}`, container);
@@ -169,6 +205,20 @@ describe('ReportUIFeatures', () => {
         expect(() => dom.find(`#uses-rel-preconnect .${checkboxClassName}`, container))
           .toThrowError('query #uses-rel-preconnect .lh-3p-filter-input not found');
       });
+
+      it('adds no filter for audits with tables containing only third party resources', () => {
+        const checkboxClassName = 'lh-3p-filter-input';
+
+        expect(() => dom.find(`#render-blocking-resources .${checkboxClassName}`, container))
+          .toThrowError('query #render-blocking-resources .lh-3p-filter-input not found');
+      });
+
+      it('adds no filter for audits with tables containing only first party resources', () => {
+        const checkboxClassName = 'lh-3p-filter-input';
+
+        expect(() => dom.find(`#uses-text-compression .${checkboxClassName}`, container))
+          .toThrowError('query #uses-text-compression .lh-3p-filter-input not found');
+      });
     });
   });
 
diff --git a/lighthouse-core/test/results/artifacts/artifacts.json b/lighthouse-core/test/results/artifacts/artifacts.json
index 93732cd0f..66b699a1f 100644
--- a/lighthouse-core/test/results/artifacts/artifacts.json
+++ b/lighthouse-core/test/results/artifacts/artifacts.json
@@ -1096,7 +1096,7 @@
         ],
         "description": "Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds",
         "help": "Elements must have sufficient color contrast",
-        "helpUrl": "https://dequeuniversity.com/rules/axe/3.1/color-contrast?application=axeAPI",
+        "helpUrl": "https://dequeuniversity.com/rules/axe/3.2/color-contrast?application=axeAPI",
         "nodes": [
           {
             "impact": "serious",
@@ -1106,7 +1106,8 @@
             ],
             "failureSummary": "Fix any of the following:\n  Element has insufficient color contrast of 1.32 (foreground color: #ffc0cb, background color: #eeeeee, font size: 18.0pt, font weight: bold). Expected contrast ratio of 3:1",
             "path": "3,HTML,1,BODY,5,DIV,0,H2",
-            "snippet": "<h2>"
+            "snippet": "<h2>",
+            "nodeLabel": "Do better web tester page"
           },
           {
             "impact": "serious",
@@ -1116,7 +1117,8 @@
             ],
             "failureSummary": "Fix any of the following:\n  Element has insufficient color contrast of 1.32 (foreground color: #ffc0cb, background color: #eeeeee, font size: 12.0pt, font weight: normal). Expected contrast ratio of 4.5:1",
             "path": "3,HTML,1,BODY,5,DIV,1,SPAN",
-            "snippet": "<span>"
+            "snippet": "<span>",
+            "nodeLabel": "Hi there!"
           }
         ]
       },
@@ -1130,7 +1132,7 @@
         ],
         "description": "Ensures every HTML document has a lang attribute",
         "help": "<html> element must have a lang attribute",
-        "helpUrl": "https://dequeuniversity.com/rules/axe/3.1/html-has-lang?application=axeAPI",
+        "helpUrl": "https://dequeuniversity.com/rules/axe/3.2/html-has-lang?application=axeAPI",
         "nodes": [
           {
             "impact": "serious",
@@ -1140,7 +1142,8 @@
             ],
             "failureSummary": "Fix any of the following:\n  The <html> element does not have a lang attribute",
             "path": "3,HTML",
-            "snippet": "<html manifest=\"clock.appcache\">"
+            "snippet": "<html manifest=\"clock.appcache\">",
+            "nodeLabel": "html"
           }
         ]
       },
@@ -1156,7 +1159,7 @@
         ],
         "description": "Ensures <img> elements have alternate text or a role of none or presentation",
         "help": "Images must have alternate text",
-        "helpUrl": "https://dequeuniversity.com/rules/axe/3.1/image-alt?application=axeAPI",
+        "helpUrl": "https://dequeuniversity.com/rules/axe/3.2/image-alt?application=axeAPI",
         "nodes": [
           {
             "impact": "critical",
@@ -1166,7 +1169,8 @@
             ],
             "failureSummary": "Fix any of the following:\n  Element does not have an alt attribute\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\"",
             "path": "3,HTML,1,BODY,10,IMG",
-            "snippet": "<img src=\"lighthouse-480x318.jpg\" width=\"480\" height=\"57\">"
+            "snippet": "<img src=\"lighthouse-480x318.jpg\" width=\"480\" height=\"57\">",
+            "nodeLabel": "img"
           },
           {
             "impact": "critical",
@@ -1176,27 +1180,30 @@
             ],
             "failureSummary": "Fix any of the following:\n  Element does not have an alt attribute\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\"",
             "path": "3,HTML,1,BODY,12,IMG",
-            "snippet": "<img src=\"lighthouse-480x318.jpg\" width=\"480\" height=\"318\">"
+            "snippet": "<img src=\"lighthouse-480x318.jpg\" width=\"480\" height=\"318\">",
+            "nodeLabel": "img"
           },
           {
             "impact": "critical",
             "html": "<img src=\"lighthouse-rotating.gif\" width=\"811\" height=\"462\">",
             "target": [
-              "img[src$=\"lighthouse-rotating.gif\"]"
+              "img[src$=\"lighthouse-rotating\\.gif\"]"
             ],
             "failureSummary": "Fix any of the following:\n  Element does not have an alt attribute\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\"",
             "path": "3,HTML,1,BODY,14,IMG",
-            "snippet": "<img src=\"lighthouse-rotating.gif\" width=\"811\" height=\"462\">"
+            "snippet": "<img src=\"lighthouse-rotating.gif\" width=\"811\" height=\"462\">",
+            "nodeLabel": "img"
           },
           {
             "impact": "critical",
             "html": "<img src=\"blob:http://localhost:62824/289254fd-ef1d-4c1a-96a8-ba291caa2140\">",
             "target": [
-              "img:nth-child(27)"
+              "img:nth-child(30)"
             ],
             "failureSummary": "Fix any of the following:\n  Element does not have an alt attribute\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\"",
-            "path": "3,HTML,1,BODY,49,IMG",
-            "snippet": "<img src=\"blob:http://localhost:62824/289254fd-ef1d-4c1a-96a8-ba291caa2140\">"
+            "path": "3,HTML,1,BODY,53,IMG",
+            "snippet": "<img src=\"blob:http://localhost:52281/459dbcc0-ca5f-47ef-b78f-e04d79c9e3fa\">",
+            "nodeLabel": "img"
           }
         ]
       },
@@ -1213,7 +1220,7 @@
         ],
         "description": "Ensures every form element has a label",
         "help": "Form elements must have labels",
-        "helpUrl": "https://dequeuniversity.com/rules/axe/3.1/label?application=axeAPI",
+        "helpUrl": "https://dequeuniversity.com/rules/axe/3.2/label?application=axeAPI",
         "nodes": [
           {
             "impact": "critical",
@@ -1222,18 +1229,20 @@
               "input[onpaste=\"event\\.preventDefault\\(\\)\\;\"]"
             ],
             "failureSummary": "Fix any of the following:\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Form element does not have an implicit (wrapped) <label>\n  Form element does not have an explicit <label>\n  Element has no title attribute or the title attribute is empty",
-            "path": "3,HTML,1,BODY,44,INPUT",
-            "snippet": "<input type=\"password\" onpaste=\"event.preventDefault();\">"
+            "path": "3,HTML,1,BODY,48,INPUT",
+            "snippet": "<input type=\"password\" onpaste=\"event.preventDefault();\">",
+            "nodeLabel": "input"
           },
           {
             "impact": "critical",
             "html": "<input type=\"password\">",
             "target": [
-              "input:nth-child(25)"
+              "input:nth-child(28)"
             ],
             "failureSummary": "Fix any of the following:\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Form element does not have an implicit (wrapped) <label>\n  Form element does not have an explicit <label>\n  Element has no title attribute or the title attribute is empty",
-            "path": "3,HTML,1,BODY,46,INPUT",
-            "snippet": "<input type=\"password\">"
+            "path": "3,HTML,1,BODY,50,INPUT",
+            "snippet": "<input type=\"password\">",
+            "nodeLabel": "input"
           },
           {
             "impact": "critical",
@@ -1242,8 +1251,9 @@
               "input[onpaste=\"return\\ false\\;\"]"
             ],
             "failureSummary": "Fix any of the following:\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Form element does not have an implicit (wrapped) <label>\n  Form element does not have an explicit <label>\n  Element has no title attribute or the title attribute is empty",
-            "path": "3,HTML,1,BODY,48,INPUT",
-            "snippet": "<input type=\"password\" onpaste=\"return false;\">"
+            "path": "3,HTML,1,BODY,52,INPUT",
+            "snippet": "<input type=\"password\" onpaste=\"return false;\">",
+            "nodeLabel": "input"
           }
         ]
       },
@@ -1260,27 +1270,29 @@
         ],
         "description": "Ensures links have discernible text",
         "help": "Links must have discernible text",
-        "helpUrl": "https://dequeuniversity.com/rules/axe/3.1/link-name?application=axeAPI",
+        "helpUrl": "https://dequeuniversity.com/rules/axe/3.2/link-name?application=axeAPI",
         "nodes": [
           {
             "impact": "serious",
             "html": "<a href=\"javascript:void(0)\" target=\"_blank\"></a>",
             "target": [
-              "a[href=\"javascript:void(0)\"]"
+              "a[href=\"javascript\\:void\\(0\\)\"]"
             ],
             "failureSummary": "Fix all of the following:\n  Element is in tab order and does not have accessible text\n\nFix any of the following:\n  Element does not have text that is visible to screen readers\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\"",
-            "path": "3,HTML,1,BODY,40,A",
-            "snippet": "<a href=\"javascript:void(0)\" target=\"_blank\">"
+            "path": "3,HTML,1,BODY,44,A",
+            "snippet": "<a href=\"javascript:void(0)\" target=\"_blank\">",
+            "nodeLabel": "a"
           },
           {
             "impact": "serious",
             "html": "<a href=\"mailto:inbox@email.com\" target=\"_blank\"></a>",
             "target": [
-              "a[href$=\"mailto:inbox@email.com\"]"
+              "a[href$=\"mailto\\:inbox\\@email\\.com\"]"
             ],
             "failureSummary": "Fix all of the following:\n  Element is in tab order and does not have accessible text\n\nFix any of the following:\n  Element does not have text that is visible to screen readers\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\"",
-            "path": "3,HTML,1,BODY,42,A",
-            "snippet": "<a href=\"mailto:inbox@email.com\" target=\"_blank\">"
+            "path": "3,HTML,1,BODY,46,A",
+            "snippet": "<a href=\"mailto:inbox@email.com\" target=\"_blank\">",
+            "nodeLabel": "a"
           }
         ]
       },
@@ -1296,7 +1308,7 @@
         ],
         "description": "Ensures <object> elements have alternate text",
         "help": "<object> elements must have alternate text",
-        "helpUrl": "https://dequeuniversity.com/rules/axe/3.1/object-alt?application=axeAPI",
+        "helpUrl": "https://dequeuniversity.com/rules/axe/3.2/object-alt?application=axeAPI",
         "nodes": [
           {
             "impact": "serious",
@@ -1304,9 +1316,10 @@
             "target": [
               "#\\35 934a"
             ],
-            "failureSummary": "Fix any of the following:\n  Element does not have text that is visible to screen readers\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty",
+            "failureSummary": "Fix any of the following:\n  Element does not have text that is visible to screen readers\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\"",
             "path": "3,HTML,1,BODY,2,OBJECT",
-            "snippet": "<object id=\"5934a\">"
+            "snippet": "<object id=\"5934a\">",
+            "nodeLabel": "object"
           },
           {
             "impact": "serious",
@@ -1314,9 +1327,10 @@
             "target": [
               "#\\35 934b"
             ],
-            "failureSummary": "Fix any of the following:\n  Element does not have text that is visible to screen readers\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty",
+            "failureSummary": "Fix any of the following:\n  Element does not have text that is visible to screen readers\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\"",
             "path": "3,HTML,1,BODY,3,OBJECT",
-            "snippet": "<object id=\"5934b\">"
+            "snippet": "<object id=\"5934b\">",
+            "nodeLabel": "object"
           }
         ]
       }
@@ -1358,7 +1372,7 @@
         ],
         "description": "Ensures elements with ARIA roles have all required ARIA attributes",
         "help": "Required ARIA attributes must be provided",
-        "helpUrl": "https://dequeuniversity.com/rules/axe/3.1/aria-required-attr?application=axeAPI",
+        "helpUrl": "https://dequeuniversity.com/rules/axe/3.2/aria-required-attr?application=axeAPI",
         "nodes": []
       },
       {
@@ -1371,7 +1385,7 @@
         ],
         "description": "Ensures elements with an ARIA role that require child roles contain them",
         "help": "Certain ARIA roles must contain particular children",
-        "helpUrl": "https://dequeuniversity.com/rules/axe/3.1/aria-required-children?application=axeAPI",
+        "helpUrl": "https://dequeuniversity.com/rules/axe/3.2/aria-required-children?application=axeAPI",
         "nodes": []
       },
       {
@@ -1384,7 +1398,7 @@
         ],
         "description": "Ensures elements with an ARIA role that require parent roles are contained by them",
         "help": "Certain ARIA roles must be contained by particular parents",
-        "helpUrl": "https://dequeuniversity.com/rules/axe/3.1/aria-required-parent?application=axeAPI",
+        "helpUrl": "https://dequeuniversity.com/rules/axe/3.2/aria-required-parent?application=axeAPI",
         "nodes": []
       },
       {
@@ -1397,7 +1411,7 @@
         ],
         "description": "Ensures all elements with a role attribute use a valid value",
         "help": "ARIA roles used must conform to valid values",
-        "helpUrl": "https://dequeuniversity.com/rules/axe/3.1/aria-roles?application=axeAPI",
+        "helpUrl": "https://dequeuniversity.com/rules/axe/3.2/aria-roles?application=axeAPI",
         "nodes": []
       },
       {
@@ -1410,7 +1424,7 @@
         ],
         "description": "Ensures all ARIA attributes have valid values",
         "help": "ARIA attributes must conform to valid values",
-        "helpUrl": "https://dequeuniversity.com/rules/axe/3.1/aria-valid-attr-value?application=axeAPI",
+        "helpUrl": "https://dequeuniversity.com/rules/axe/3.2/aria-valid-attr-value?application=axeAPI",
         "nodes": []
       },
       {
@@ -1423,7 +1437,7 @@
         ],
         "description": "Ensures attributes that begin with aria- are valid ARIA attributes",
         "help": "ARIA attributes must conform to valid names",
-        "helpUrl": "https://dequeuniversity.com/rules/axe/3.1/aria-valid-attr?application=axeAPI",
+        "helpUrl": "https://dequeuniversity.com/rules/axe/3.2/aria-valid-attr?application=axeAPI",
         "nodes": []
       },
       {
@@ -1466,7 +1480,7 @@
         ],
         "description": "Ensures <dl> elements are structured correctly",
         "help": "<dl> elements must only directly contain properly-ordered <dt> and <dd> groups, <script> or <template> elements",
-        "helpUrl": "https://dequeuniversity.com/rules/axe/3.1/definition-list?application=axeAPI",
+        "helpUrl": "https://dequeuniversity.com/rules/axe/3.2/definition-list?application=axeAPI",
         "nodes": []
       },
       {
@@ -1521,7 +1535,7 @@
         ],
         "description": "Ensures <iframe> and <frame> elements contain a non-empty title attribute",
         "help": "Frames must have title attribute",
-        "helpUrl": "https://dequeuniversity.com/rules/axe/3.1/frame-title?application=axeAPI",
+        "helpUrl": "https://dequeuniversity.com/rules/axe/3.2/frame-title?application=axeAPI",
         "nodes": []
       },
       {
@@ -1562,7 +1576,7 @@
         ],
         "description": "Ensures <input type=\"image\"> elements have alternate text",
         "help": "Image buttons must have alternate text",
-        "helpUrl": "https://dequeuniversity.com/rules/axe/3.1/input-image-alt?application=axeAPI",
+        "helpUrl": "https://dequeuniversity.com/rules/axe/3.2/input-image-alt?application=axeAPI",
         "nodes": []
       },
       {
@@ -1575,7 +1589,7 @@
         ],
         "description": "Ensures presentational <table> elements do not use <th>, <caption> elements or the summary attribute",
         "help": "Layout tables must not use data table elements",
-        "helpUrl": "https://dequeuniversity.com/rules/axe/3.1/layout-table?application=axeAPI",
+        "helpUrl": "https://dequeuniversity.com/rules/axe/3.2/layout-table?application=axeAPI",
         "nodes": []
       },
       {
@@ -1588,7 +1602,7 @@
         ],
         "description": "Ensures that lists are structured correctly",
         "help": "<ul> and <ol> must only directly contain <li>, <script> or <template> elements",
-        "helpUrl": "https://dequeuniversity.com/rules/axe/3.1/list?application=axeAPI",
+        "helpUrl": "https://dequeuniversity.com/rules/axe/3.2/list?application=axeAPI",
         "nodes": []
       },
       {
@@ -1601,7 +1615,7 @@
         ],
         "description": "Ensures <li> elements are used semantically",
         "help": "<li> elements must be contained in a <ul> or <ol>",
-        "helpUrl": "https://dequeuniversity.com/rules/axe/3.1/listitem?application=axeAPI",
+        "helpUrl": "https://dequeuniversity.com/rules/axe/3.2/listitem?application=axeAPI",
         "nodes": []
       },
       {
@@ -1617,7 +1631,7 @@
         ],
         "description": "Ensures <meta http-equiv=\"refresh\"> is not used",
         "help": "Timed refresh must not exist",
-        "helpUrl": "https://dequeuniversity.com/rules/axe/3.1/meta-refresh?application=axeAPI",
+        "helpUrl": "https://dequeuniversity.com/rules/axe/3.2/meta-refresh?application=axeAPI",
         "nodes": []
       },
       {
@@ -1629,7 +1643,7 @@
         ],
         "description": "Ensures tabindex attribute values are not greater than 0",
         "help": "Elements should not have tabindex greater than zero",
-        "helpUrl": "https://dequeuniversity.com/rules/axe/3.1/tabindex?application=axeAPI",
+        "helpUrl": "https://dequeuniversity.com/rules/axe/3.2/tabindex?application=axeAPI",
         "nodes": []
       },
       {
@@ -1644,7 +1658,7 @@
         ],
         "description": "Ensure that each cell in a table using the headers refers to another cell in that table",
         "help": "All cells in a table element that use the headers attribute must only refer to other cells of that same table",
-        "helpUrl": "https://dequeuniversity.com/rules/axe/3.1/td-headers-attr?application=axeAPI",
+        "helpUrl": "https://dequeuniversity.com/rules/axe/3.2/td-headers-attr?application=axeAPI",
         "nodes": []
       },
       {
@@ -1659,7 +1673,7 @@
         ],
         "description": "Ensure that each table header in a data table refers to data cells",
         "help": "All th elements and elements with role=columnheader/rowheader must have data cells they describe",
-        "helpUrl": "https://dequeuniversity.com/rules/axe/3.1/th-has-data-cells?application=axeAPI",
+        "helpUrl": "https://dequeuniversity.com/rules/axe/3.2/th-has-data-cells?application=axeAPI",
         "nodes": []
       },
       {
@@ -1672,7 +1686,7 @@
         ],
         "description": "Ensures lang attributes have valid values",
         "help": "lang attribute must have a valid value",
-        "helpUrl": "https://dequeuniversity.com/rules/axe/3.1/valid-lang?application=axeAPI",
+        "helpUrl": "https://dequeuniversity.com/rules/axe/3.2/valid-lang?application=axeAPI",
         "nodes": []
       },
       {
@@ -1687,7 +1701,7 @@
         ],
         "description": "Ensures <video> elements have captions",
         "help": "<video> elements must have captions",
-        "helpUrl": "https://dequeuniversity.com/rules/axe/3.1/video-caption?application=axeAPI",
+        "helpUrl": "https://dequeuniversity.com/rules/axe/3.2/video-caption?application=axeAPI",
         "nodes": []
       },
       {
@@ -1702,7 +1716,7 @@
         ],
         "description": "Ensures <video> elements have audio descriptions",
         "help": "<video> elements must have an audio description track",
-        "helpUrl": "https://dequeuniversity.com/rules/axe/3.1/video-description?application=axeAPI",
+        "helpUrl": "https://dequeuniversity.com/rules/axe/3.2/video-description?application=axeAPI",
         "nodes": []
       }
     ]
@@ -2017,6 +2031,7 @@
       "snippet": "<button class=\"small-button\">Do something</button>",
       "path": "3,HTML,1,BODY,17,BUTTON",
       "selector": "body > button.small-button",
+      "nodeLabel": "Do something",
       "href": ""
     },
     {
@@ -2033,6 +2048,7 @@
       "snippet": "<button class=\"small-button\">Do something else</button>",
       "path": "3,HTML,1,BODY,18,BUTTON",
       "selector": "body > button.small-button",
+      "nodeLabel": "Do something else",
       "href": ""
     }
   ],
diff --git a/lighthouse-core/test/results/sample-config.js b/lighthouse-core/test/results/sample-config.js
index 9f8144dbb..8f63818ce 100644
--- a/lighthouse-core/test/results/sample-config.js
+++ b/lighthouse-core/test/results/sample-config.js
@@ -42,7 +42,7 @@ const budgetedConfig = {
         {metric: 'first-cpu-idle', budget: 2900, tolerance: 100},
         {metric: 'interactive', budget: 2900, tolerance: 100},
         {metric: 'first-meaningful-paint', budget: 2000, tolerance: 100},
-        {metric: 'estimated-input-latency', budget: 100, tolerance: 100},
+        {metric: 'max-potential-fid', budget: 100, tolerance: 100},
       ],
     }],
   },
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index 19844e8e8..ad1b5d0b8 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -1598,7 +1598,8 @@
               "selector": "h2",
               "path": "3,HTML,1,BODY,5,DIV,0,H2",
               "snippet": "<h2>Do better web tester page</h2>",
-              "explanation": "Fix any of the following:\n  Element has insufficient color contrast of 1.32 (foreground color: #ffc0cb, background color: #eeeeee, font size: 18.0pt, font weight: bold). Expected contrast ratio of 3:1"
+              "explanation": "Fix any of the following:\n  Element has insufficient color contrast of 1.32 (foreground color: #ffc0cb, background color: #eeeeee, font size: 18.0pt, font weight: bold). Expected contrast ratio of 3:1",
+              "nodeLabel": "Do better web tester page"
             }
           },
           {
@@ -1607,7 +1608,8 @@
               "selector": "span",
               "path": "3,HTML,1,BODY,5,DIV,1,SPAN",
               "snippet": "<span>Hi there!</span>",
-              "explanation": "Fix any of the following:\n  Element has insufficient color contrast of 1.32 (foreground color: #ffc0cb, background color: #eeeeee, font size: 12.0pt, font weight: normal). Expected contrast ratio of 4.5:1"
+              "explanation": "Fix any of the following:\n  Element has insufficient color contrast of 1.32 (foreground color: #ffc0cb, background color: #eeeeee, font size: 12.0pt, font weight: normal). Expected contrast ratio of 4.5:1",
+              "nodeLabel": "Hi there!"
             }
           }
         ],
@@ -1689,7 +1691,8 @@
               "selector": "html",
               "path": "3,HTML",
               "snippet": "<html manifest=\"clock.appcache\">",
-              "explanation": "Fix any of the following:\n  The <html> element does not have a lang attribute"
+              "explanation": "Fix any of the following:\n  The <html> element does not have a lang attribute",
+              "nodeLabel": "html"
             }
           }
         ],
@@ -1733,7 +1736,8 @@
               "selector": "img[height=\"\\35 7\"]",
               "path": "3,HTML,1,BODY,10,IMG",
               "snippet": "<img src=\"lighthouse-480x318.jpg\" width=\"480\" height=\"57\">",
-              "explanation": "Fix any of the following:\n  Element does not have an alt attribute\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\""
+              "explanation": "Fix any of the following:\n  Element does not have an alt attribute\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\"",
+              "nodeLabel": "img"
             }
           },
           {
@@ -1742,25 +1746,28 @@
               "selector": "img[height=\"\\33 18\"]",
               "path": "3,HTML,1,BODY,12,IMG",
               "snippet": "<img src=\"lighthouse-480x318.jpg\" width=\"480\" height=\"318\">",
-              "explanation": "Fix any of the following:\n  Element does not have an alt attribute\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\""
+              "explanation": "Fix any of the following:\n  Element does not have an alt attribute\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\"",
+              "nodeLabel": "img"
             }
           },
           {
             "node": {
               "type": "node",
-              "selector": "img[src$=\"lighthouse-rotating.gif\"]",
+              "selector": "img[src$=\"lighthouse-rotating\\.gif\"]",
               "path": "3,HTML,1,BODY,14,IMG",
               "snippet": "<img src=\"lighthouse-rotating.gif\" width=\"811\" height=\"462\">",
-              "explanation": "Fix any of the following:\n  Element does not have an alt attribute\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\""
+              "explanation": "Fix any of the following:\n  Element does not have an alt attribute\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\"",
+              "nodeLabel": "img"
             }
           },
           {
             "node": {
               "type": "node",
-              "selector": "img:nth-child(27)",
-              "path": "3,HTML,1,BODY,49,IMG",
+              "selector": "img:nth-child(30)",
+              "path": "3,HTML,1,BODY,53,IMG",
               "snippet": "<img src=\"blob:http://localhost:62824/289254fd-ef1d-4c1a-96a8-ba291caa2140\">",
-              "explanation": "Fix any of the following:\n  Element does not have an alt attribute\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\""
+              "explanation": "Fix any of the following:\n  Element does not have an alt attribute\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\"",
+              "nodeLabel": "img"
             }
           }
         ],
@@ -1804,27 +1811,30 @@
             "node": {
               "type": "node",
               "selector": "input[onpaste=\"event\\.preventDefault\\(\\)\\;\"]",
-              "path": "3,HTML,1,BODY,44,INPUT",
+              "path": "3,HTML,1,BODY,48,INPUT",
               "snippet": "<input type=\"password\" onpaste=\"event.preventDefault();\">",
-              "explanation": "Fix any of the following:\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Form element does not have an implicit (wrapped) <label>\n  Form element does not have an explicit <label>\n  Element has no title attribute or the title attribute is empty"
+              "explanation": "Fix any of the following:\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Form element does not have an implicit (wrapped) <label>\n  Form element does not have an explicit <label>\n  Element has no title attribute or the title attribute is empty",
+              "nodeLabel": "input"
             }
           },
           {
             "node": {
               "type": "node",
-              "selector": "input:nth-child(25)",
-              "path": "3,HTML,1,BODY,46,INPUT",
+              "selector": "input:nth-child(28)",
+              "path": "3,HTML,1,BODY,50,INPUT",
               "snippet": "<input type=\"password\">",
-              "explanation": "Fix any of the following:\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Form element does not have an implicit (wrapped) <label>\n  Form element does not have an explicit <label>\n  Element has no title attribute or the title attribute is empty"
+              "explanation": "Fix any of the following:\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Form element does not have an implicit (wrapped) <label>\n  Form element does not have an explicit <label>\n  Element has no title attribute or the title attribute is empty",
+              "nodeLabel": "input"
             }
           },
           {
             "node": {
               "type": "node",
               "selector": "input[onpaste=\"return\\ false\\;\"]",
-              "path": "3,HTML,1,BODY,48,INPUT",
+              "path": "3,HTML,1,BODY,52,INPUT",
               "snippet": "<input type=\"password\" onpaste=\"return false;\">",
-              "explanation": "Fix any of the following:\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Form element does not have an implicit (wrapped) <label>\n  Form element does not have an explicit <label>\n  Element has no title attribute or the title attribute is empty"
+              "explanation": "Fix any of the following:\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Form element does not have an implicit (wrapped) <label>\n  Form element does not have an explicit <label>\n  Element has no title attribute or the title attribute is empty",
+              "nodeLabel": "input"
             }
           }
         ],
@@ -1868,19 +1878,21 @@
           {
             "node": {
               "type": "node",
-              "selector": "a[href=\"javascript:void(0)\"]",
-              "path": "3,HTML,1,BODY,40,A",
+              "selector": "a[href=\"javascript\\:void\\(0\\)\"]",
+              "path": "3,HTML,1,BODY,44,A",
               "snippet": "<a href=\"javascript:void(0)\" target=\"_blank\"></a>",
-              "explanation": "Fix all of the following:\n  Element is in tab order and does not have accessible text\n\nFix any of the following:\n  Element does not have text that is visible to screen readers\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\""
+              "explanation": "Fix all of the following:\n  Element is in tab order and does not have accessible text\n\nFix any of the following:\n  Element does not have text that is visible to screen readers\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\"",
+              "nodeLabel": "a"
             }
           },
           {
             "node": {
               "type": "node",
-              "selector": "a[href$=\"mailto:inbox@email.com\"]",
-              "path": "3,HTML,1,BODY,42,A",
+              "selector": "a[href$=\"mailto\\:inbox\\@email\\.com\"]",
+              "path": "3,HTML,1,BODY,46,A",
               "snippet": "<a href=\"mailto:inbox@email.com\" target=\"_blank\"></a>",
-              "explanation": "Fix all of the following:\n  Element is in tab order and does not have accessible text\n\nFix any of the following:\n  Element does not have text that is visible to screen readers\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\""
+              "explanation": "Fix all of the following:\n  Element is in tab order and does not have accessible text\n\nFix any of the following:\n  Element does not have text that is visible to screen readers\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\"",
+              "nodeLabel": "a"
             }
           }
         ],
@@ -1953,7 +1965,8 @@
               "selector": "#\\35 934a",
               "path": "3,HTML,1,BODY,2,OBJECT",
               "snippet": "<object id=\"5934a\"></object>",
-              "explanation": "Fix any of the following:\n  Element does not have text that is visible to screen readers\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty"
+              "explanation": "Fix any of the following:\n  Element does not have text that is visible to screen readers\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\"",
+              "nodeLabel": "object"
             }
           },
           {
@@ -1962,7 +1975,8 @@
               "selector": "#\\35 934b",
               "path": "3,HTML,1,BODY,3,OBJECT",
               "snippet": "<object id=\"5934b\"></object>",
-              "explanation": "Fix any of the following:\n  Element does not have text that is visible to screen readers\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty"
+              "explanation": "Fix any of the following:\n  Element does not have text that is visible to screen readers\n  aria-label attribute does not exist or is empty\n  aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n  Element has no title attribute or the title attribute is empty\n  Element's default semantics were not overridden with role=\"presentation\"\n  Element's default semantics were not overridden with role=\"none\"",
+              "nodeLabel": "object"
             }
           }
         ],
@@ -3113,13 +3127,15 @@
               "type": "node",
               "snippet": "<button class=\"small-button\">Do something</button>",
               "path": "3,HTML,1,BODY,17,BUTTON",
-              "selector": "body > button.small-button"
+              "selector": "body > button.small-button",
+              "nodeLabel": "Do something"
             },
             "overlappingTarget": {
               "type": "node",
               "snippet": "<button class=\"small-button\">Do something else</button>",
               "path": "3,HTML,1,BODY,18,BUTTON",
-              "selector": "body > button.small-button"
+              "selector": "body > button.small-button",
+              "nodeLabel": "Do something else"
             },
             "tapTargetScore": 864,
             "overlappingTargetScore": 720,
@@ -3290,7 +3306,7 @@
             "tolerance": 100
           },
           {
-            "metric": "estimated-input-latency",
+            "metric": "max-potential-fid",
             "budget": 100,
             "tolerance": 100
           }