Repository: GoogleChrome/lighthouse
Pull request #9410 (2019-07-18 20:59:42)

clients(lightrider): serialize errors in artifacts
LR specific changes for #9397

brendankenny: (let's move against master)

Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7b4d425a0..b956c8a72 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,6 +2,10 @@
 
 We'd love your help! This doc covers how to become a contributor and submit code to the project.
 
+## Where can I start?
+
+We tag issues that are good candidates for those new to the code with [`good first issue`](https://github.com/GoogleChrome/lighthouse/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22). We recommend you start there!
+
 ## Follow the coding style
 
 The `.eslintrc` file defines all. We use [JSDoc](http://usejsdoc.org/) with [TypeScript](https://github.com/Microsoft/TypeScript/wiki/JSDoc-support-in-JavaScript). Annotations are encouraged for all contributions.
diff --git a/clients/lightrider/lightrider-entry.js b/clients/lightrider/lightrider-entry.js
index 49cfa5561..e7ea5a64d 100644
--- a/clients/lightrider/lightrider-entry.js
+++ b/clients/lightrider/lightrider-entry.js
@@ -9,6 +9,7 @@ const lighthouse = require('../../lighthouse-core/index.js');
 
 const LHError = require('../../lighthouse-core/lib/lh-error.js');
 const preprocessor = require('../../lighthouse-core/lib/proto-preprocessor.js');
+const assetSaver = require('../../lighthouse-core/lib/asset-saver.js');
 
 /** @type {Record<'mobile'|'desktop', LH.Config.Json>} */
 const LR_PRESETS = {
@@ -54,15 +55,22 @@ async function runLighthouseInLR(connection, url, flags, lrOpts) {
     const runnerResult = await lighthouse(url, flags, config, connection);
     if (!runnerResult) throw new Error('Lighthouse finished without a runnerResult');
 
+    // pre process the LHR for proto
+    const preprocessedLhr = preprocessor.processForProto(runnerResult.lhr);
+
     // When LR is called with |internal: {keep_raw_response: true, save_lighthouse_assets: true}|,
-    // this code will log artifacts to raw_response.artifacts.
+    // we log artifacts to raw_response.artifacts.
     if (logAssets) {
-      // @ts-ignore - piggyback the artifacts on the LHR.
-      runnerResult.lhr.artifacts = runnerResult.artifacts;
+      // Properly serialize artifact errors.
+      const artifactsJson = JSON.stringify(runnerResult.artifacts, assetSaver.stringifyReplacer);
+
+      return JSON.stringify({
+        ...preprocessedLhr,
+        artifacts: JSON.parse(artifactsJson),
+      });
     }
 
-    // pre process the LHR for proto
-    return JSON.stringify(preprocessor.processForProto(runnerResult.lhr));
+    return JSON.stringify(preprocessedLhr);
   } catch (err) {
     // If an error ruined the entire lighthouse run, attempt to return a meaningful error.
     let runtimeError;
diff --git a/lighthouse-core/audits/metrics.js b/lighthouse-core/audits/metrics.js
index 374517c21..98a50d46e 100644
--- a/lighthouse-core/audits/metrics.js
+++ b/lighthouse-core/audits/metrics.js
@@ -14,7 +14,7 @@ const FirstCPUIdle = require('../computed/metrics/first-cpu-idle.js');
 const Interactive = require('../computed/metrics/interactive.js');
 const SpeedIndex = require('../computed/metrics/speed-index.js');
 const EstimatedInputLatency = require('../computed/metrics/estimated-input-latency.js');
-const CumulativeLongQueuingDelay = require('../computed/metrics/cumulative-long-queuing-delay.js');
+const TotalBlockingTime = require('../computed/metrics/total-blocking-time.js');
 
 class Metrics extends Audit {
   /**
@@ -60,7 +60,7 @@ class Metrics extends Audit {
     const interactive = await requestOrUndefined(Interactive, metricComputationData);
     const speedIndex = await requestOrUndefined(SpeedIndex, metricComputationData);
     const estimatedInputLatency = await EstimatedInputLatency.request(metricComputationData, context); // eslint-disable-line max-len
-    const cumulativeLongQueuingDelay = await CumulativeLongQueuingDelay.request(metricComputationData, context); // eslint-disable-line max-len
+    const totalBlockingTime = await TotalBlockingTime.request(metricComputationData, context); // eslint-disable-line max-len
 
     /** @type {UberMetricsItem} */
     const metrics = {
@@ -77,7 +77,7 @@ class Metrics extends Audit {
       speedIndexTs: speedIndex && speedIndex.timestamp,
       estimatedInputLatency: estimatedInputLatency.timing,
       estimatedInputLatencyTs: estimatedInputLatency.timestamp,
-      cumulativeLongQueuingDelay: cumulativeLongQueuingDelay.timing,
+      totalBlockingTime: totalBlockingTime.timing,
 
       // Include all timestamps of interest from trace of tab
       observedNavigationStart: traceOfTab.timings.navigationStart,
@@ -140,7 +140,7 @@ class Metrics extends Audit {
  * @property {number=} speedIndexTs
  * @property {number} estimatedInputLatency
  * @property {number=} estimatedInputLatencyTs
- * @property {number} cumulativeLongQueuingDelay
+ * @property {number} totalBlockingTime
  * @property {number} observedNavigationStart
  * @property {number} observedNavigationStartTs
  * @property {number=} observedFirstPaint
diff --git a/lighthouse-core/audits/metrics/cumulative-long-queuing-delay.js b/lighthouse-core/audits/metrics/total-blocking-time.js
similarity index 52%
rename from lighthouse-core/audits/metrics/cumulative-long-queuing-delay.js
rename to lighthouse-core/audits/metrics/total-blocking-time.js
index dd0297ad8..174e5d5d5 100644
--- a/lighthouse-core/audits/metrics/cumulative-long-queuing-delay.js
+++ b/lighthouse-core/audits/metrics/total-blocking-time.js
@@ -6,24 +6,28 @@
 'use strict';
 
 const Audit = require('../audit.js');
-const CumulativeLQD = require('../../computed/metrics/cumulative-long-queuing-delay.js');
+const ComputedTBT = require('../../computed/metrics/total-blocking-time.js');
+const i18n = require('../../lib/i18n/i18n.js');
 
-// TODO(deepanjanroy): i18n strings once metric is final.
-const UIStringsNotExported = {
-  title: 'Cumulative Long Queuing Delay',
-  description: '[Experimental metric] Total time period between FCP and Time to Interactive ' +
-      'during which queuing time for any input event would be higher than 50ms.',
+const UIStrings = {
+  /** The name of a metric that calculates the total duration of blocking time for a web page. Blocking times are time periods when the page would be blocked (prevented) from responding to user input (clicks, taps, and keypresses will feel slow to respond). Shown to users as the label for the numeric metric value. Ideally fits within a ~40 character limit. */
+  title: 'Total Blocking Time',
+  /** Description of the Total Blocking Time (TBT) metric, which calculates the total duration of blocking time for a web page. Blocking times are time periods when the page would be blocked (prevented) from responding to user input (clicks, taps, and keypresses will feel slow to respond). This is displayed within a tooltip when the user hovers on the metric name to see more. No character length limits.*/
+  description: 'Sum of all time periods between FCP and Time to Interactive, ' +
+      'when task length exceeded 50ms, expressed in milliseconds.',
 };
 
-class CumulativeLongQueuingDelay extends Audit {
+const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
+
+class TotalBlockingTime extends Audit {
   /**
    * @return {LH.Audit.Meta}
    */
   static get meta() {
     return {
-      id: 'cumulative-long-queuing-delay',
-      title: UIStringsNotExported.title,
-      description: UIStringsNotExported.description,
+      id: 'total-blocking-time',
+      title: str_(UIStrings.title),
+      description: str_(UIStrings.description),
       scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
       requiredArtifacts: ['traces', 'devtoolsLogs'],
     };
@@ -46,13 +50,12 @@ class CumulativeLongQueuingDelay extends Audit {
   }
 
   /**
-   * Audits the page to calculate Cumulative Long Queuing Delay.
+   * Audits the page to calculate Total Blocking Time.
    *
-   * We define Long Queuing Delay Region as any time interval in the loading timeline where queuing
-   * time for an input event would be longer than 50ms. For example, if there is a 110ms main thread
-   * task, the first 60ms of it is Long Queuing Delay Region, because any input event occuring in
-   * that region has to wait more than 50ms. Cumulative Long Queuing Delay is the sum of all Long
-   * Queuing Delay Regions between First Contentful Paint and Interactive Time (TTI).
+   * We define Blocking Time as any time interval in the loading timeline where task length exceeds
+   * 50ms. For example, if there is a 110ms main thread task, the last 60ms of it is blocking time.
+   * Total Blocking Time is the sum of all Blocking Time between First Contentful Paint and
+   * Interactive Time (TTI).
    *
    * @param {LH.Artifacts} artifacts
    * @param {LH.Audit.Context} context
@@ -62,7 +65,7 @@ class CumulativeLongQueuingDelay extends Audit {
     const trace = artifacts.traces[Audit.DEFAULT_PASS];
     const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
     const metricComputationData = {trace, devtoolsLog, settings: context.settings};
-    const metricResult = await CumulativeLQD.request(metricComputationData, context);
+    const metricResult = await ComputedTBT.request(metricComputationData, context);
 
     return {
       score: Audit.computeLogNormalScore(
@@ -71,9 +74,10 @@ class CumulativeLongQueuingDelay extends Audit {
         context.options.scoreMedian
       ),
       numericValue: metricResult.timing,
-      displayValue: 10 * Math.round(metricResult.timing / 10) + '\xa0ms',
+      displayValue: str_(i18n.UIStrings.ms, {timeInMs: metricResult.timing}),
     };
   }
 }
 
-module.exports = CumulativeLongQueuingDelay;
+module.exports = TotalBlockingTime;
+module.exports.UIStrings = UIStrings;
diff --git a/lighthouse-core/audits/seo/font-size.js b/lighthouse-core/audits/seo/font-size.js
index 3e36b9588..c3dca56bd 100644
--- a/lighthouse-core/audits/seo/font-size.js
+++ b/lighthouse-core/audits/seo/font-size.js
@@ -25,8 +25,6 @@ const UIStrings = {
   /** Explanatory message stating that there was a failure in an audit caused by a missing page viewport meta tag configuration. "viewport" and "meta" are HTML terms and should not be translated. */
   explanationViewport: 'Text is illegible because there\'s no viewport meta tag optimized ' +
     'for mobile screens.',
-  /** Explanatory message stating that there was a failure in an audit caused by a certain percentage of the text on the page being too small. "decimalProportion" will be replaced by a percentage between 0 and 100%. */
-  explanation: '{decimalProportion, number, extendedPercent} of text is too small.',
   /** Explanatory message stating that there was a failure in an audit caused by a certain percentage of the text on the page being too small, based on a sample size of text that was less than 100% of the text on the page. "decimalProportion" will be replaced by a percentage between 0 and 100%. */
   explanationWithDisclaimer: '{decimalProportion, number, extendedPercent} of text is too ' +
     'small (based on {decimalProportionVisited, number, extendedPercent} sample).',
@@ -328,9 +326,6 @@ class FontSize extends Audit {
             decimalProportion: percentageOfFailingText,
             decimalProportionVisited: percentageOfVisitedText,
           });
-      } else {
-        explanation = str_(UIStrings.explanation,
-          {decimalProportion: percentageOfFailingText});
       }
     }
 
diff --git a/lighthouse-core/computed/metrics/lantern-cumulative-long-queuing-delay.js b/lighthouse-core/computed/metrics/lantern-total-blocking-time.js
similarity index 82%
rename from lighthouse-core/computed/metrics/lantern-cumulative-long-queuing-delay.js
rename to lighthouse-core/computed/metrics/lantern-total-blocking-time.js
index 112b3539f..214f5c425 100644
--- a/lighthouse-core/computed/metrics/lantern-cumulative-long-queuing-delay.js
+++ b/lighthouse-core/computed/metrics/lantern-total-blocking-time.js
@@ -13,7 +13,7 @@ const LanternInteractive = require('./lantern-interactive.js');
 
 /** @typedef {BaseNode.Node} Node */
 
-class LanternCumulativeLongQueuingDelay extends LanternMetric {
+class LanternTotalBlockingTime extends LanternMetric {
   /**
    * @return {LH.Gatherer.Simulation.MetricCoefficients}
    */
@@ -48,32 +48,32 @@ class LanternCumulativeLongQueuingDelay extends LanternMetric {
    */
   static getEstimateFromSimulation(simulation, extras) {
     // Intentionally use the opposite FCP estimate. A pessimistic FCP is higher than equal to an
-    // optimistic FCP, which means potentially more tasks are excluded from the
-    // CumulativeLongQueuingDelay computation. So a more pessimistic FCP gives a more optimistic
-    // CumulativeLongQueuingDelay for the same work.
+    // optimistic FCP, which means potentially more tasks are excluded from the Total Blocking Time
+    // computation. So a more pessimistic FCP gives a more optimistic Total Blocking Time for the
+    // same work.
     const fcpTimeInMs = extras.optimistic
       ? extras.fcpResult.pessimisticEstimate.timeInMs
       : extras.fcpResult.optimisticEstimate.timeInMs;
 
     // Similarly, we always have pessimistic TTI >= optimistic TTI. Therefore, picking optimistic
     // TTI means our window of interest is smaller and thus potentially more tasks are excluded from
-    // CumulativeLongQueuingDelay computation, yielding a lower (more optimistic)
-    // CumulativeLongQueuingDelay value for the same work.
+    // Total Blocking Time computation, yielding a lower (more optimistic) Total Blocking Time value
+    // for the same work.
     const interactiveTimeMs = extras.optimistic
       ? extras.interactiveResult.optimisticEstimate.timeInMs
       : extras.interactiveResult.pessimisticEstimate.timeInMs;
 
     // Require here to resolve circular dependency.
-    const CumulativeLongQueuingDelay = require('./cumulative-long-queuing-delay.js');
-    const minDurationMs = CumulativeLongQueuingDelay.LONG_QUEUING_DELAY_THRESHOLD;
+    const TotalBlockingTime = require('./total-blocking-time.js');
+    const minDurationMs = TotalBlockingTime.BLOCKING_TIME_THRESHOLD;
 
-    const events = LanternCumulativeLongQueuingDelay.getTopLevelEvents(
+    const events = LanternTotalBlockingTime.getTopLevelEvents(
       simulation.nodeTimings,
       minDurationMs
     );
 
     return {
-      timeInMs: CumulativeLongQueuingDelay.calculateSumOfLongQueuingDelay(
+      timeInMs: TotalBlockingTime.calculateSumOfBlockingTime(
         events,
         fcpTimeInMs,
         interactiveTimeMs
@@ -118,4 +118,4 @@ class LanternCumulativeLongQueuingDelay extends LanternMetric {
   }
 }
 
-module.exports = makeComputedArtifact(LanternCumulativeLongQueuingDelay);
+module.exports = makeComputedArtifact(LanternTotalBlockingTime);
diff --git a/lighthouse-core/computed/metrics/cumulative-long-queuing-delay.js b/lighthouse-core/computed/metrics/total-blocking-time.js
similarity index 51%
rename from lighthouse-core/computed/metrics/cumulative-long-queuing-delay.js
rename to lighthouse-core/computed/metrics/total-blocking-time.js
index 18aa99d15..d5419203d 100644
--- a/lighthouse-core/computed/metrics/cumulative-long-queuing-delay.js
+++ b/lighthouse-core/computed/metrics/total-blocking-time.js
@@ -9,28 +9,27 @@ const makeComputedArtifact = require('../computed-artifact.js');
 const ComputedMetric = require('./metric.js');
 const LHError = require('../../lib/lh-error.js');
 const TracingProcessor = require('../../lib/tracehouse/trace-processor.js');
-const LanternCumulativeLongQueuingDelay = require('./lantern-cumulative-long-queuing-delay.js');
+const LanternTotalBlockingTime = require('./lantern-total-blocking-time.js');
 const TimetoInteractive = require('./interactive.js');
 
 /**
- * @fileoverview This audit determines Cumulative Long Queuing Delay between FCP and TTI.
+ * @fileoverview This audit determines Total Blocking Time.
 
- * We define Long Queuing Delay Region as any time interval in the loading timeline where queuing
- * time for an input event would be longer than 50ms. For example, if there is a 110ms main thread
- * task, the first 60ms of it is Long Queuing Delay Region, because any input event occuring in
- * that region has to wait more than 50ms. Cumulative Long Queuing Delay is the sum of all Long
- * Queuing Delay Regions between First Contentful Paint and Interactive Time (TTI).
+ * We define Blocking Time as any time interval in the loading timeline where task length exceeds
+ * 50ms. For example, if there is a 110ms main thread task, the last 60ms of it is blocking time.
+ * Total Blocking Time is the sum of all Blocking Time between First Contentful Paint and
+ * Interactive Time (TTI).
  *
  * This is a new metric designed to accompany Time to Interactive. TTI is strict and does not
  * reflect incremental improvements to the site performance unless the improvement concerns the last
- * long task. Cumulative Long Queuing Delay on the other hand is designed to be much more responsive
+ * long task. Total Blocking Time on the other hand is designed to be much more responsive
  * to smaller improvements to main thread responsiveness.
  */
-class CumulativeLongQueuingDelay extends ComputedMetric {
+class TotalBlockingTime extends ComputedMetric {
   /**
    * @return {number}
    */
-  static get LONG_QUEUING_DELAY_THRESHOLD() {
+  static get BLOCKING_TIME_THRESHOLD() {
     return 50;
   }
   /**
@@ -39,50 +38,43 @@ class CumulativeLongQueuingDelay extends ComputedMetric {
    * @param {number} interactiveTimeMs
    * @return {number}
    */
-  static calculateSumOfLongQueuingDelay(topLevelEvents, fcpTimeInMs, interactiveTimeMs) {
+  static calculateSumOfBlockingTime(topLevelEvents, fcpTimeInMs, interactiveTimeMs) {
     if (interactiveTimeMs <= fcpTimeInMs) return 0;
 
-    const threshold = CumulativeLongQueuingDelay.LONG_QUEUING_DELAY_THRESHOLD;
-    const longQueuingDelayRegions = [];
-    // First identifying the long queuing delay regions.
+    const threshold = TotalBlockingTime.BLOCKING_TIME_THRESHOLD;
+    let sumBlockingTime = 0;
     for (const event of topLevelEvents) {
-      // If the task is less than the delay threshold, it contains no Long Queuing Delay Region.
+      // Early exit for small tasks, which should far outnumber long tasks.
       if (event.duration < threshold) continue;
-      // Otherwise, the duration of the task before the delay-threshold-sized interval at the end is
-      // considered Long Queuing Delay Region. Example assuming the threshold is 50ms:
-      //   [              250ms Task                   ]
-      //   |  Long Queuing Delay Region  |   Last 50ms |
-      //               200 ms
-      longQueuingDelayRegions.push({
-        start: event.start,
-        end: event.end - threshold,
-        duration: event.duration - threshold,
-      });
-    }
 
-    let sumLongQueuingDelay = 0;
-    for (const region of longQueuingDelayRegions) {
-      // We only want to add up the Long Queuing Delay regions that fall between FCP and TTI.
-      //
+      // We only want to consider tasks that fall between FCP and TTI.
       // FCP is picked as the lower bound because there is little risk of user input happening
       // before FCP so Long Queuing Qelay regions do not harm user experience. Developers should be
       // optimizing to reach FCP as fast as possible without having to worry about task lengths.
-      //
+      if (event.end < fcpTimeInMs) continue;
+
       // TTI is picked as the upper bound because we want a well defined end point so that the
       // metric does not rely on how long we trace.
-      if (region.end < fcpTimeInMs) continue;
-      if (region.start > interactiveTimeMs) continue;
+      if (event.start > interactiveTimeMs) continue;
 
-      // If a Long Queuing Delay Region spans the edges of our region of interest, we clip it to
-      // only include the part of the region that falls inside.
-      const clippedStart = Math.max(region.start, fcpTimeInMs);
-      const clippedEnd = Math.min(region.end, interactiveTimeMs);
-      const queuingDelayAfterClipping = clippedEnd - clippedStart;
+      // We first perform the clipping, and then calculate Blocking Region. So if we have a 150ms
+      // task [0, 150] and FCP happens midway at 50ms, we first clip the task to [50, 150], and then
+      // calculate the Blocking Region to be [100, 150]. The rational here is that tasks before FCP
+      // are unimportant, so we care whether the main thread is busy more than 50ms at a time only
+      // after FCP.
+      const clippedStart = Math.max(event.start, fcpTimeInMs);
+      const clippedEnd = Math.min(event.end, interactiveTimeMs);
+      const clippedDuration = clippedEnd - clippedStart;
+      if (clippedDuration < threshold) continue;
 
-      sumLongQueuingDelay += queuingDelayAfterClipping;
+      // The duration of the task beyond 50ms at the beginning is considered the Blocking Region.
+      // Example:
+      //   [              250ms Task                   ]
+      //   | First 50ms |   Blocking Region (200ms)    |
+      sumBlockingTime += (clippedDuration - threshold);
     }
 
-    return sumLongQueuingDelay;
+    return sumBlockingTime;
   }
 
   /**
@@ -91,7 +83,7 @@ class CumulativeLongQueuingDelay extends ComputedMetric {
    * @return {Promise<LH.Artifacts.LanternMetric>}
    */
   static computeSimulatedMetric(data, context) {
-    return LanternCumulativeLongQueuingDelay.request(data, context);
+    return LanternTotalBlockingTime.request(data, context);
   }
 
   /**
@@ -107,12 +99,10 @@ class CumulativeLongQueuingDelay extends ComputedMetric {
 
     const interactiveTimeMs = (await TimetoInteractive.request(data, context)).timing;
 
-    // Not using the start time argument of getMainThreadTopLevelEvents, because
-    // we need to clip the part of the task before the last 50ms properly.
     const events = TracingProcessor.getMainThreadTopLevelEvents(data.traceOfTab);
 
     return {
-      timing: CumulativeLongQueuingDelay.calculateSumOfLongQueuingDelay(
+      timing: TotalBlockingTime.calculateSumOfBlockingTime(
         events,
         firstContentfulPaint,
         interactiveTimeMs
@@ -121,4 +111,4 @@ class CumulativeLongQueuingDelay extends ComputedMetric {
   }
 }
 
-module.exports = makeComputedArtifact(CumulativeLongQueuingDelay);
+module.exports = makeComputedArtifact(TotalBlockingTime);
diff --git a/lighthouse-core/config/default-config.js b/lighthouse-core/config/default-config.js
index 76c749273..e77251978 100644
--- a/lighthouse-core/config/default-config.js
+++ b/lighthouse-core/config/default-config.js
@@ -171,7 +171,7 @@ const defaultConfig = {
     'screenshot-thumbnails',
     'final-screenshot',
     'metrics/estimated-input-latency',
-    'metrics/cumulative-long-queuing-delay',
+    'metrics/total-blocking-time',
     'metrics/max-potential-fid',
     'errors-in-console',
     'time-to-first-byte',
@@ -369,7 +369,7 @@ const defaultConfig = {
         {id: 'first-cpu-idle', weight: 2, group: 'metrics'},
         {id: 'max-potential-fid', weight: 0, group: 'metrics'},
         {id: 'estimated-input-latency', weight: 0}, // intentionally left out of metrics so it won't be displayed
-        {id: 'cumulative-long-queuing-delay', weight: 0}, // intentionally left out of metrics so it won't be displayed
+        {id: 'total-blocking-time', weight: 0}, // intentionally left out of metrics so it won't be displayed
         {id: 'render-blocking-resources', weight: 0, group: 'load-opportunities'},
         {id: 'uses-responsive-images', weight: 0, group: 'load-opportunities'},
         {id: 'offscreen-images', weight: 0, group: 'load-opportunities'},
diff --git a/lighthouse-core/lib/asset-saver.js b/lighthouse-core/lib/asset-saver.js
index 8ff4c65ba..5490bcfae 100644
--- a/lighthouse-core/lib/asset-saver.js
+++ b/lighthouse-core/lib/asset-saver.js
@@ -288,4 +288,5 @@ module.exports = {
   prepareAssets,
   saveTrace,
   saveLanternNetworkData,
+  stringifyReplacer,
 };
diff --git a/lighthouse-core/lib/i18n/en-US.json b/lighthouse-core/lib/i18n/en-US.json
index c9c19ed8d..18aac00f8 100644
--- a/lighthouse-core/lib/i18n/en-US.json
+++ b/lighthouse-core/lib/i18n/en-US.json
@@ -1039,6 +1039,14 @@
     "message": "Speed Index",
     "description": "The name of the metric that summarizes how quickly the page looked visually complete. The name of this metric is largely abstract and can be loosely translated. Shown to users as the label for the numeric metric value. Ideally fits within a ~40 character limit."
   },
+  "lighthouse-core/audits/metrics/total-blocking-time.js | description": {
+    "message": "Sum of all time periods between FCP and Time to Interactive, when task length exceeded 50ms, expressed in milliseconds.",
+    "description": "Description of the Total Blocking Time (TBT) metric, which calculates the total duration of blocking time for a web page. Blocking times are time periods when the page would be blocked (prevented) from responding to user input (clicks, taps, and keypresses will feel slow to respond). This is displayed within a tooltip when the user hovers on the metric name to see more. No character length limits."
+  },
+  "lighthouse-core/audits/metrics/total-blocking-time.js | title": {
+    "message": "Total Blocking Time",
+    "description": "The name of a metric that calculates the total duration of blocking time for a web page. Blocking times are time periods when the page would be blocked (prevented) from responding to user input (clicks, taps, and keypresses will feel slow to respond). Shown to users as the label for the numeric metric value. Ideally fits within a ~40 character limit."
+  },
   "lighthouse-core/audits/network-rtt.js | description": {
     "message": "Network round trip times (RTT) have a large impact on performance. If the RTT to an origin is high, it's an indication that servers closer to the user could improve performance. [Learn more](https://hpbn.co/primer-on-latency-and-bandwidth/).",
     "description": "Description of a Lighthouse audit that tells the user that a high network round trip time (RTT) can effect their website's performance because the server is physically far away from them thus making the RTT high. This is displayed after a user expands the section to see more. No character length limits. 'Learn More' becomes link text to additional documentation."
@@ -1135,10 +1143,6 @@
     "message": "{decimalProportion, number, extendedPercent} legible text",
     "description": "Label for the audit identifying font sizes that are too small."
   },
-  "lighthouse-core/audits/seo/font-size.js | explanation": {
-    "message": "{decimalProportion, number, extendedPercent} of text is too small.",
-    "description": "Explanatory message stating that there was a failure in an audit caused by a certain percentage of the text on the page being too small. \"decimalProportion\" will be replaced by a percentage between 0 and 100%."
-  },
   "lighthouse-core/audits/seo/font-size.js | explanationViewport": {
     "message": "Text is illegible because there's no viewport meta tag optimized for mobile screens.",
     "description": "Explanatory message stating that there was a failure in an audit caused by a missing page viewport meta tag configuration. \"viewport\" and \"meta\" are HTML terms and should not be translated."
diff --git a/lighthouse-core/lib/i18n/locales/en-XL.json b/lighthouse-core/lib/i18n/locales/en-XL.json
index 41d675b7e..9b7b75729 100644
--- a/lighthouse-core/lib/i18n/locales/en-XL.json
+++ b/lighthouse-core/lib/i18n/locales/en-XL.json
@@ -779,6 +779,12 @@
   "lighthouse-core/audits/metrics/speed-index.js | title": {
     "message": "Ŝṕêéd̂ Ín̂d́êx́"
   },
+  "lighthouse-core/audits/metrics/total-blocking-time.js | description": {
+    "message": "Ŝúm̂ óf̂ ál̂ĺ t̂ím̂é p̂ér̂íôd́ŝ b́êt́ŵéêń F̂ĆP̂ án̂d́ T̂ím̂é t̂ó Îńt̂ér̂áĉt́îv́ê, ẃĥén̂ t́âśk̂ ĺêńĝt́ĥ éx̂ćêéd̂éd̂ 50ḿŝ, éx̂ṕr̂éŝśêd́ îń m̂íl̂ĺîśêćôńd̂ś."
+  },
+  "lighthouse-core/audits/metrics/total-blocking-time.js | title": {
+    "message": "T̂ót̂ál̂ B́l̂óĉḱîńĝ T́îḿê"
+  },
   "lighthouse-core/audits/network-rtt.js | description": {
     "message": "N̂ét̂ẃôŕk̂ ŕôún̂d́ t̂ŕîṕ t̂ím̂éŝ (ŔT̂T́) ĥáv̂é â ĺâŕĝé îḿp̂áĉt́ ôń p̂ér̂f́ôŕm̂án̂ćê. Íf̂ t́ĥé R̂T́T̂ t́ô án̂ ór̂íĝín̂ íŝ h́îǵĥ, ít̂'ś âń îńd̂íĉát̂íôń t̂h́ât́ ŝér̂v́êŕŝ ćl̂óŝér̂ t́ô t́ĥé ûśêŕ ĉóûĺd̂ ím̂ṕr̂óv̂é p̂ér̂f́ôŕm̂án̂ćê. [Ĺêár̂ń m̂ór̂é](ĥt́t̂ṕŝ://h́p̂b́n̂.ćô/ṕr̂ím̂ér̂-ón̂-ĺât́êńĉý-âńd̂-b́âńd̂ẃîd́t̂h́/)."
   },
@@ -851,9 +857,6 @@
   "lighthouse-core/audits/seo/font-size.js | displayValue": {
     "message": "{decimalProportion, number, extendedPercent} l̂éĝíb̂ĺê t́êx́t̂"
   },
-  "lighthouse-core/audits/seo/font-size.js | explanation": {
-    "message": "{decimalProportion, number, extendedPercent} ôf́ t̂éx̂t́ îś t̂óô śm̂ál̂ĺ."
-  },
   "lighthouse-core/audits/seo/font-size.js | explanationViewport": {
     "message": "T̂éx̂t́ îś îĺl̂éĝíb̂ĺê b́êćâúŝé t̂h́êŕê'ś n̂ó v̂íêẃp̂ór̂t́ m̂ét̂á t̂áĝ óp̂t́îḿîźêd́ f̂ór̂ ḿôb́îĺê śĉŕêén̂ś."
   },
diff --git a/lighthouse-core/report/html/report-styles.css b/lighthouse-core/report/html/report-styles.css
index 5e3d387ed..880c39b16 100644
--- a/lighthouse-core/report/html/report-styles.css
+++ b/lighthouse-core/report/html/report-styles.css
@@ -951,8 +951,9 @@
 }
 
 .lh-audit-explanation {
-  margin: var(--audit-padding-vertical) 0 calc(var(--audit-padding-vertical) / 2);
+  margin: var(--audit-padding-vertical) 0 calc(var(--audit-padding-vertical) / 2) var(--audit-margin-horizontal);
   line-height: var(--audit-explanation-line-height);
+  display: inline-block;
 }
 
 .lh-audit--fail .lh-audit-explanation {
diff --git a/lighthouse-core/report/html/templates.html b/lighthouse-core/report/html/templates.html
index 26687d46d..a418bf9f1 100644
--- a/lighthouse-core/report/html/templates.html
+++ b/lighthouse-core/report/html/templates.html
@@ -45,7 +45,7 @@ limitations under the License.
 <!-- Lighthouse category header -->
 <template id="tmpl-lh-category-header">
   <div class="lh-category-header">
-    <div class="lh-score__gauge"></div>
+    <div class="lh-score__gauge" role="heading" aria-level="2"></div>
     <div class="lh-category-header__description"></div>
   </div>
 </template>
diff --git a/lighthouse-core/scripts/build-report-for-autodeployment.js b/lighthouse-core/scripts/build-report-for-autodeployment.js
index 1fd9c2904..ea1260a86 100644
--- a/lighthouse-core/scripts/build-report-for-autodeployment.js
+++ b/lighthouse-core/scripts/build-report-for-autodeployment.js
@@ -19,7 +19,7 @@ const {defaultSettings} = require('../config/constants.js');
 const lighthouse = require('../index.js');
 const lhr = /** @type {LH.Result} */ (require('../../lighthouse-core/test/results/sample_v2.json'));
 
-const DIST = path.join(__dirname, `../../dist`);
+const DIST = path.join(__dirname, `../../dist/now`);
 
 (async function() {
   addPluginCategory(lhr);
diff --git a/now.json b/now.json
index 02b220f9d..9e04006d2 100644
--- a/now.json
+++ b/now.json
@@ -3,7 +3,8 @@
   "builds": [
     {
       "src": "package.json",
-      "use": "@now/static-build"
+      "use": "@now/static-build",
+      "config": {"distDir": "dist/now"}
     }
   ],
   "github": {
diff --git a/proto/sample_v2_round_trip.json b/proto/sample_v2_round_trip.json
index 4ad9bc15d..63b760193 100644
--- a/proto/sample_v2_round_trip.json
+++ b/proto/sample_v2_round_trip.json
@@ -377,15 +377,6 @@
             "scoreDisplayMode": "informative",
             "title": "Minimize Critical Requests Depth"
         },
-        "cumulative-long-queuing-delay": {
-            "description": "[Experimental metric] Total time period between FCP and Time to Interactive during which queuing time for any input event would be higher than 50ms.",
-            "displayValue": "120\u00a0ms",
-            "id": "cumulative-long-queuing-delay",
-            "numericValue": 116.79800000000023,
-            "score": 1.0,
-            "scoreDisplayMode": "numeric",
-            "title": "Cumulative Long Queuing Delay"
-        },
         "custom-controls-labels": {
             "description": "Custom interactive controls have associated labels, provided by aria-label or aria-labelledby. [Learn more](https://developers.google.com/web/fundamentals/accessibility/how-to-review#try_it_with_a_screen_reader).",
             "id": "custom-controls-labels",
@@ -1494,7 +1485,6 @@
             "details": {
                 "items": [
                     {
-                        "cumulativeLongQueuingDelay": 117.0,
                         "estimatedInputLatency": 16.0,
                         "firstCPUIdle": 4927.0,
                         "firstCPUIdleTs": 185608247190.0,
@@ -1525,7 +1515,8 @@
                         "observedTraceEnd": 10281.0,
                         "observedTraceEndTs": 185613601189.0,
                         "speedIndex": 4417.0,
-                        "speedIndexTs": 185607736912.0
+                        "speedIndexTs": 185607736912.0,
+                        "totalBlockingTime": 117.0
                     }
                 ],
                 "type": "debugdata"
@@ -2629,6 +2620,15 @@
             "scoreDisplayMode": "binary",
             "title": "Server response times are low (TTFB)"
         },
+        "total-blocking-time": {
+            "description": "Sum of all time periods between FCP and Time to Interactive, when task length exceeded 50ms, expressed in milliseconds.",
+            "displayValue": "120\u00a0ms",
+            "id": "total-blocking-time",
+            "numericValue": 116.79800000000023,
+            "score": 1.0,
+            "scoreDisplayMode": "numeric",
+            "title": "Total Blocking Time"
+        },
         "total-byte-weight": {
             "description": "Large network payloads cost users real money and are highly correlated with long load times. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/network-payloads).",
             "details": {
@@ -3546,7 +3546,7 @@
                     "weight": 0.0
                 },
                 {
-                    "id": "cumulative-long-queuing-delay",
+                    "id": "total-blocking-time",
                     "weight": 0.0
                 },
                 {
@@ -4180,13 +4180,13 @@
             {
                 "duration": 100.0,
                 "entryType": "measure",
-                "name": "lh:audit:cumulative-long-queuing-delay",
+                "name": "lh:audit:total-blocking-time",
                 "startTime": 0.0
             },
             {
                 "duration": 100.0,
                 "entryType": "measure",
-                "name": "lh:computed:CumulativeLongQueuingDelay",
+                "name": "lh:computed:TotalBlockingTime",
                 "startTime": 0.0
             },
             {


Test patch:
diff --git a/clients/test/lightrider-entry-test.js b/clients/test/lightrider-entry-test.js
index 7042d47fd..a81c526be 100644
--- a/clients/test/lightrider-entry-test.js
+++ b/clients/test/lightrider-entry-test.js
@@ -118,5 +118,32 @@ describe('lightrider-entry', () => {
 
       runStub.mockRestore();
     });
+
+    it('exposes artifacts when logAssets is true', async () => {
+      const originalRun = Runner.run;
+      Runner.run = jest.fn().mockReturnValue(Promise.resolve({
+        lhr: {},
+        artifacts: {
+          Artifact: new Error('some error'),
+        },
+      }));
+      afterEach(() => {
+        Runner.run = originalRun;
+      });
+
+      const mockConnection = {};
+      const url = 'https://example.com';
+      const lrFlags = {
+        logAssets: true,
+      };
+      const resultJson = await lhBackground.runLighthouseInLR(mockConnection, url, {}, lrFlags);
+      const result = JSON.parse(resultJson);
+      expect(result.artifacts).toMatchObject({
+        Artifact: {
+          sentinel: '__ErrorSentinel',
+          message: 'some error',
+        },
+      });
+    });
   });
 });
diff --git a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
index 2daeb8136..dbb9cbc02 100644
--- a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
+++ b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
@@ -43,7 +43,7 @@ Object {
       "path": "metrics/estimated-input-latency",
     },
     Object {
-      "path": "metrics/cumulative-long-queuing-delay",
+      "path": "metrics/total-blocking-time",
     },
     Object {
       "path": "metrics/max-potential-fid",
@@ -717,7 +717,7 @@ Object {
           "weight": 0,
         },
         Object {
-          "id": "cumulative-long-queuing-delay",
+          "id": "total-blocking-time",
           "weight": 0,
         },
         Object {
diff --git a/lighthouse-cli/test/smokehouse/a11y/expectations.js b/lighthouse-cli/test/smokehouse/a11y/expectations.js
index 214b4fee2..d8692e30e 100644
--- a/lighthouse-cli/test/smokehouse/a11y/expectations.js
+++ b/lighthouse-cli/test/smokehouse/a11y/expectations.js
@@ -153,7 +153,8 @@ module.exports = [
                   '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',
+                  // Default font size is different depending on the platform (e.g. 28.5 on travis, 30.0 on Mac), so use \d\d\.\d.
+                  'explanation': /^Fix any of the following:\n {2}Element has insufficient color contrast of 2\.59 \(foreground color: #ffc0cb, background color: #ff0000, font size: \d\d\.\dpt, font weight: normal\). Expected contrast ratio of 3:1$/,
                   'nodeLabel': 'Hello',
                 },
               },
diff --git a/lighthouse-core/test/audits/__snapshots__/metrics-test.js.snap b/lighthouse-core/test/audits/__snapshots__/metrics-test.js.snap
index 685d98388..f79601e53 100644
--- a/lighthouse-core/test/audits/__snapshots__/metrics-test.js.snap
+++ b/lighthouse-core/test/audits/__snapshots__/metrics-test.js.snap
@@ -2,7 +2,6 @@
 
 exports[`Performance: metrics evaluates valid input correctly 1`] = `
 Object {
-  "cumulativeLongQueuingDelay": 748,
   "estimatedInputLatency": 78,
   "estimatedInputLatencyTs": undefined,
   "firstCPUIdle": 3351,
@@ -35,5 +34,6 @@ Object {
   "observedTraceEndTs": 225426711887,
   "speedIndex": 1657,
   "speedIndexTs": undefined,
+  "totalBlockingTime": 726,
 }
 `;
diff --git a/lighthouse-core/test/audits/dobetterweb/no-vulnerable-libraries-test.js b/lighthouse-core/test/audits/dobetterweb/no-vulnerable-libraries-test.js
index ef65ff78c..33f1c60fe 100644
--- a/lighthouse-core/test/audits/dobetterweb/no-vulnerable-libraries-test.js
+++ b/lighthouse-core/test/audits/dobetterweb/no-vulnerable-libraries-test.js
@@ -8,6 +8,7 @@
 const NoVulnerableLibrariesAudit =
   require('../../../audits/dobetterweb/no-vulnerable-libraries.js');
 const assert = require('assert');
+const semver = require('semver');
 
 /* eslint-env jest */
 describe('Avoids front-end JavaScript libraries with known vulnerabilities', () => {
@@ -99,3 +100,55 @@ describe('Avoids front-end JavaScript libraries with known vulnerabilities', ()
     assert.equal(auditResult.score, 1);
   });
 });
+
+describe('Snyk database', () => {
+  // https://github.com/npm/node-semver/issues/166#issuecomment-245990039
+  function hasUpperBound(rangeString) {
+    const range = new semver.Range(rangeString);
+    if (!range) return false;
+
+    // For every subset ...
+    for (const subset of range.set) {
+    // Upperbound exists if...
+
+    // < or <= is in one of the subset's clauses (= gets normalized to >= and <).
+      if (subset.some(comparator => comparator.operator && comparator.operator.match(/^</))) {
+        continue;
+      }
+
+      // Subset has a prerelease tag (operator will be empty string).
+      if (subset.length === 1 && subset[0].operator === '') {
+        continue;
+      }
+
+      // No upperbound for this subset.
+      return false;
+    }
+
+    return true;
+  }
+
+  it('hasUpperBound works as intended', () => {
+    assert.equal(hasUpperBound('<1.12.2'), true);
+    assert.equal(hasUpperBound('=1.12.2'), true);
+    assert.equal(hasUpperBound('>=1.12.3 <2.2.2'), true);
+    assert.equal(hasUpperBound('>=2.2.3 <3.0.0'), true);
+    assert.equal(hasUpperBound('>=3.0.0 <3.10.1 || =3.10.2'), true);
+
+    assert.equal(hasUpperBound('>1.12.2'), false);
+    assert.equal(hasUpperBound('>=1.12.2'), false);
+    assert.equal(hasUpperBound('*'), false);
+  });
+
+  it('every snyk vulnerability has an upper bound', () => {
+    for (const vulns of Object.values(NoVulnerableLibrariesAudit.snykDB.npm)) {
+      for (const vuln of vulns) {
+        for (const semver of vuln.semver.vulnerable) {
+          if (!hasUpperBound(semver)) {
+            assert.fail(`invalid semver: ${semver}. Must contain an upper bound`);
+          }
+        }
+      }
+    }
+  });
+});
diff --git a/lighthouse-core/test/audits/metrics/cumulative-long-queuing-delay-test.js b/lighthouse-core/test/audits/metrics/total-blocking-time-test.js
similarity index 71%
rename from lighthouse-core/test/audits/metrics/cumulative-long-queuing-delay-test.js
rename to lighthouse-core/test/audits/metrics/total-blocking-time-test.js
index 160322069..1720753ee 100644
--- a/lighthouse-core/test/audits/metrics/cumulative-long-queuing-delay-test.js
+++ b/lighthouse-core/test/audits/metrics/total-blocking-time-test.js
@@ -5,25 +5,25 @@
  */
 'use strict';
 
-const cLQDAudit = require('../../../audits/metrics/cumulative-long-queuing-delay.js');
-const options = cLQDAudit.defaultOptions;
+const TBTAudit = require('../../../audits/metrics/total-blocking-time.js');
+const options = TBTAudit.defaultOptions;
 
 const pwaTrace = require('../../fixtures/traces/progressive-app-m60.json');
 
 function generateArtifactsWithTrace(trace) {
   return {
-    traces: {[cLQDAudit.DEFAULT_PASS]: trace},
-    devtoolsLogs: {[cLQDAudit.DEFAULT_PASS]: []},
+    traces: {[TBTAudit.DEFAULT_PASS]: trace},
+    devtoolsLogs: {[TBTAudit.DEFAULT_PASS]: []},
   };
 }
 /* eslint-env jest */
 
-describe('Performance: cumulative-long-queuing-delay audit', () => {
-  it('evaluates cumulative long queuing delay metric properly', async () => {
+describe('Performance: total-blocking-time audit', () => {
+  it('evaluates Total Blocking Time metric properly', async () => {
     const artifacts = generateArtifactsWithTrace(pwaTrace);
     const settings = {throttlingMethod: 'provided'};
     const context = {options, settings, computedCache: new Map()};
-    const output = await cLQDAudit.audit(artifacts, context);
+    const output = await TBTAudit.audit(artifacts, context);
 
     expect(output.numericValue).toBeCloseTo(48.3, 1);
     expect(output.score).toBe(1);
diff --git a/lighthouse-core/test/audits/seo/font-size-test.js b/lighthouse-core/test/audits/seo/font-size-test.js
index 83138f049..236266153 100644
--- a/lighthouse-core/test/audits/seo/font-size-test.js
+++ b/lighthouse-core/test/audits/seo/font-size-test.js
@@ -54,7 +54,6 @@ describe('SEO: Font size audit', () => {
 
     const auditResult = await FontSizeAudit.audit(artifacts, getFakeContext());
     assert.equal(auditResult.score, 0);
-    expect(auditResult.explanation).toBeDisplayString('41% of text is too small.');
     expect(auditResult.displayValue).toBeDisplayString('59% legible text');
   });
 
@@ -238,10 +237,10 @@ describe('SEO: Font size audit', () => {
     expect(auditResult.notApplicable).toBe(true);
   });
 
-  describe('attributes source location', () => {
+  describe('attributes source of style', () => {
     async function runFontSizeAuditWithSingleFailingStyle(style, nodeProperties) {
       const artifacts = {
-        URL: {finalUrl: 'http://www.example.com'},
+        URL,
         MetaElements: makeMetaElements(validViewport),
         FontSize: {
           analyzedFailingNodesData: [
@@ -264,10 +263,13 @@ describe('SEO: Font size audit', () => {
         attributes: ['class', 'my-p'],
       });
 
-      expect(auditResult.details.items[0].selector).toMatchObject({
-        type: 'node',
-        selector: '#my-parent',
-        snippet: '<p class="my-p">',
+      expect(auditResult.details.items[0]).toMatchObject({
+        source: URL.finalUrl,
+        selector: {
+          type: 'node',
+          selector: '#my-parent',
+          snippet: '<p class="my-p">',
+        },
       });
     });
 
@@ -280,10 +282,13 @@ describe('SEO: Font size audit', () => {
         attributes: ['size', '10px'],
       });
 
-      expect(auditResult.details.items[0].selector).toMatchObject({
-        type: 'node',
-        selector: '#my-parent',
-        snippet: '<font size="10px">',
+      expect(auditResult.details.items[0]).toMatchObject({
+        source: URL.finalUrl,
+        selector: {
+          type: 'node',
+          selector: '#my-parent',
+          snippet: '<font size="10px">',
+        },
       });
     });
   });
diff --git a/lighthouse-core/test/computed/metrics/cumulative-long-queuing-delay-test.js b/lighthouse-core/test/computed/metrics/cumulative-long-queuing-delay-test.js
deleted file mode 100644
index 8b02ada00..000000000
--- a/lighthouse-core/test/computed/metrics/cumulative-long-queuing-delay-test.js
+++ /dev/null
@@ -1,115 +0,0 @@
-/**
- * @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 CumulativeLongQueuingDelay =
-    require('../../../computed/metrics/cumulative-long-queuing-delay.js');
-const trace = require('../../fixtures/traces/progressive-app-m60.json');
-const devtoolsLog = require('../../fixtures/traces/progressive-app-m60.devtools.log.json');
-
-/* eslint-env jest */
-
-describe('Metrics: CumulativeLongQueuingDelay', () => {
-  it('should compute a simulated value', async () => {
-    const settings = {throttlingMethod: 'simulate'};
-    const context = {settings, computedCache: new Map()};
-    const result = await CumulativeLongQueuingDelay.request(
-      {trace, devtoolsLog, settings},
-      context
-    );
-
-    expect({
-      timing: Math.round(result.timing),
-      optimistic: Math.round(result.optimisticEstimate.timeInMs),
-      pessimistic: Math.round(result.pessimisticEstimate.timeInMs),
-    }).toMatchInlineSnapshot(`
-Object {
-  "optimistic": 719,
-  "pessimistic": 777,
-  "timing": 748,
-}
-`);
-  });
-
-  it('should compute an observed value', async () => {
-    const settings = {throttlingMethod: 'provided'};
-    const context = {settings, computedCache: new Map()};
-    const result = await CumulativeLongQueuingDelay.request(
-      {trace, devtoolsLog, settings},
-      context
-    );
-    expect(result.timing).toBeCloseTo(48.3, 1);
-  });
-
-  describe('#calculateSumOfLongQueuingDelay', () => {
-    it('reports 0 when no task is longer than 50ms', () => {
-      const events = [
-        {start: 1000, end: 1050, duration: 50},
-        {start: 2000, end: 2010, duration: 10},
-      ];
-
-      const fcpTimeMs = 500;
-      const interactiveTimeMs = 4000;
-
-      expect(CumulativeLongQueuingDelay.calculateSumOfLongQueuingDelay(
-        events,
-        fcpTimeMs,
-        interactiveTimeMs
-      )).toBe(0);
-    });
-
-    it('only looks at tasks within FMP and TTI', () => {
-      const events = [
-        {start: 1000, end: 1060, duration: 60},
-        {start: 2000, end: 2100, duration: 100},
-        {start: 2300, end: 2450, duration: 150},
-        {start: 2600, end: 2800, duration: 200},
-      ];
-
-      const fcpTimeMs = 1500;
-      const interactiveTimeMs = 2500;
-
-      expect(CumulativeLongQueuingDelay.calculateSumOfLongQueuingDelay(
-        events,
-        fcpTimeMs,
-        interactiveTimeMs
-      )).toBe(150);
-    });
-
-    it('clips queuing delay regions properly', () => {
-      const fcpTimeMs = 1050;
-      const interactiveTimeMs = 2050;
-
-      const events = [
-        {start: 1000, end: 1110, duration: 110}, // Contributes 10ms.
-        {start: 2000, end: 2200, duration: 200}, // Contributes 50ms.
-      ];
-
-      expect(CumulativeLongQueuingDelay.calculateSumOfLongQueuingDelay(
-        events,
-        fcpTimeMs,
-        interactiveTimeMs
-      )).toBe(60);
-    });
-
-    // This can happen in the lantern metric case, where we use the optimistic
-    // TTI and pessimistic FCP.
-    it('returns 0 if interactiveTime is earlier than FCP', () => {
-      const fcpTimeMs = 2050;
-      const interactiveTimeMs = 1050;
-
-      const events = [
-        {start: 500, end: 3000, duration: 2500},
-      ];
-
-      expect(CumulativeLongQueuingDelay.calculateSumOfLongQueuingDelay(
-        events,
-        fcpTimeMs,
-        interactiveTimeMs
-      )).toBe(0);
-    });
-  });
-});
diff --git a/lighthouse-core/test/computed/metrics/total-blocking-time-test.js b/lighthouse-core/test/computed/metrics/total-blocking-time-test.js
new file mode 100644
index 000000000..15e9fcff5
--- /dev/null
+++ b/lighthouse-core/test/computed/metrics/total-blocking-time-test.js
@@ -0,0 +1,148 @@
+/**
+ * @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 TotalBlockingTime = require('../../../computed/metrics/total-blocking-time.js');
+const trace = require('../../fixtures/traces/progressive-app-m60.json');
+const devtoolsLog = require('../../fixtures/traces/progressive-app-m60.devtools.log.json');
+
+/* eslint-env jest */
+
+describe('Metrics: TotalBlockingTime', () => {
+  it('should compute a simulated value', async () => {
+    const settings = {throttlingMethod: 'simulate'};
+    const context = {settings, computedCache: new Map()};
+    const result = await TotalBlockingTime.request({trace, devtoolsLog, settings}, context);
+
+    expect({
+      timing: Math.round(result.timing),
+      optimistic: Math.round(result.optimisticEstimate.timeInMs),
+      pessimistic: Math.round(result.pessimisticEstimate.timeInMs),
+    }).toMatchInlineSnapshot(`
+Object {
+  "optimistic": 676,
+  "pessimistic": 777,
+  "timing": 726,
+}
+`);
+  });
+
+  it('should compute an observed value', async () => {
+    const settings = {throttlingMethod: 'provided'};
+    const context = {settings, computedCache: new Map()};
+    const result = await TotalBlockingTime.request({trace, devtoolsLog, settings}, context);
+    expect(result.timing).toBeCloseTo(48.3, 1);
+  });
+
+  describe('#calculateSumOfBlockingTime', () => {
+    it('reports 0 when no task is longer than 50ms', () => {
+      const events = [
+        {start: 1000, end: 1050, duration: 50},
+        {start: 2000, end: 2010, duration: 10},
+      ];
+
+      const fcpTimeMs = 500;
+      const interactiveTimeMs = 4000;
+
+      expect(
+        TotalBlockingTime.calculateSumOfBlockingTime(events, fcpTimeMs, interactiveTimeMs)
+      ).toBe(0);
+    });
+
+    it('only looks at tasks within FCP and TTI', () => {
+      const events = [
+        {start: 1000, end: 1060, duration: 60},
+        {start: 2000, end: 2100, duration: 100},
+        {start: 2300, end: 2450, duration: 150},
+        {start: 2600, end: 2800, duration: 200},
+      ];
+
+      const fcpTimeMs = 1500;
+      const interactiveTimeMs = 2500;
+
+      expect(
+        TotalBlockingTime.calculateSumOfBlockingTime(events, fcpTimeMs, interactiveTimeMs)
+      ).toBe(150);
+    });
+
+    it('clips before finding blocking regions', () => {
+      const fcpTimeMs = 150;
+      const interactiveTimeMs = 300;
+
+      const events = [
+        // The clipping is done first, so the task becomes [150, 200] after clipping and contributes
+        // 0ms of blocking time. This is in contrast to first calculating the blocking region ([100,
+        // 200]) and then clipping at FCP (150ms), which yields 50ms blocking time.
+        {start: 50, end: 200, duration: 150},
+        // Similarly, the task is first clipped above to be [240, 300], and then contributes 10ms
+        // blocking time.
+        {start: 240, end: 460, duration: 120},
+      ];
+
+      expect(
+        TotalBlockingTime.calculateSumOfBlockingTime(events, fcpTimeMs, interactiveTimeMs)
+      ).toBe(10); // 0ms + 10ms.
+    });
+
+    // TTI can happen in the middle of a task, for example, if TTI is at FMP which occurs as part
+    // of a larger task, or in the lantern case where we use estimate TTI using a different graph
+    // from the one used to estimate TBT.
+    it('clips properly if TTI falls in the middle of a task', () => {
+      const fcpTimeMs = 1000;
+      const interactiveTimeMs = 2000;
+
+      expect(TotalBlockingTime.calculateSumOfBlockingTime(
+        [{start: 1951, end: 2100, duration: 149}],
+        fcpTimeMs,
+        interactiveTimeMs
+      )).toBe(0); // Duration after clipping is 49, which is < 50.
+      expect(TotalBlockingTime.calculateSumOfBlockingTime(
+        [{start: 1950, end: 2100, duration: 150}],
+        fcpTimeMs,
+        interactiveTimeMs
+      )).toBe(0); // Duration after clipping is 50, so time after 50ms is 0ms.
+      expect(TotalBlockingTime.calculateSumOfBlockingTime(
+        [{start: 1949, end: 2100, duration: 151}],
+        fcpTimeMs,
+        interactiveTimeMs
+      )).toBe(1); // Duration after clipping is 51, so time after 50ms is 1ms.
+    });
+
+    it('clips properly if FCP falls in the middle of a task', () => {
+      const fcpTimeMs = 1000;
+      const interactiveTimeMs = 2000;
+
+      expect(TotalBlockingTime.calculateSumOfBlockingTime(
+        [{start: 900, end: 1049, duration: 149}],
+        fcpTimeMs,
+        interactiveTimeMs
+      )).toBe(0); // Duration after clipping is 49, which is < 50.
+      expect(TotalBlockingTime.calculateSumOfBlockingTime(
+        [{start: 900, end: 1050, duration: 150}],
+        fcpTimeMs,
+        interactiveTimeMs
+      )).toBe(0); // Duration after clipping is 50, so time after 50ms is 0ms.
+      expect(TotalBlockingTime.calculateSumOfBlockingTime(
+        [{start: 900, end: 1051, duration: 151}],
+        fcpTimeMs,
+        interactiveTimeMs
+      )).toBe(1); // Duration after clipping is 51, so time after 50ms is 1ms.
+    });
+
+    // This can happen in the lantern metric case, where we use the optimistic
+    // TTI and pessimistic FCP.
+    it('returns 0 if interactiveTime is earlier than FCP', () => {
+      const fcpTimeMs = 2050;
+      const interactiveTimeMs = 1050;
+
+      const events = [{start: 500, end: 3000, duration: 2500}];
+
+      expect(
+        TotalBlockingTime.calculateSumOfBlockingTime(events, fcpTimeMs, interactiveTimeMs)
+      ).toBe(0);
+    });
+  });
+});
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index 082ce4041..a68818bf5 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -191,10 +191,10 @@
       "numericValue": 16,
       "displayValue": "20 ms"
     },
-    "cumulative-long-queuing-delay": {
-      "id": "cumulative-long-queuing-delay",
-      "title": "Cumulative Long Queuing Delay",
-      "description": "[Experimental metric] Total time period between FCP and Time to Interactive during which queuing time for any input event would be higher than 50ms.",
+    "total-blocking-time": {
+      "id": "total-blocking-time",
+      "title": "Total Blocking Time",
+      "description": "Sum of all time periods between FCP and Time to Interactive, when task length exceeded 50ms, expressed in milliseconds.",
       "score": 1,
       "scoreDisplayMode": "numeric",
       "numericValue": 116.79800000000023,
@@ -1258,7 +1258,7 @@
             "speedIndex": 4417,
             "speedIndexTs": 185607736912,
             "estimatedInputLatency": 16,
-            "cumulativeLongQueuingDelay": 117,
+            "totalBlockingTime": 117,
             "observedNavigationStart": 0,
             "observedNavigationStartTs": 185603319912,
             "observedFirstPaint": 3969,
@@ -3417,7 +3417,7 @@
           "weight": 0
         },
         {
-          "id": "cumulative-long-queuing-delay",
+          "id": "total-blocking-time",
           "weight": 0
         },
         {
@@ -4283,13 +4283,13 @@
       },
       {
         "startTime": 0,
-        "name": "lh:audit:cumulative-long-queuing-delay",
+        "name": "lh:audit:total-blocking-time",
         "duration": 100,
         "entryType": "measure"
       },
       {
         "startTime": 0,
-        "name": "lh:computed:CumulativeLongQueuingDelay",
+        "name": "lh:computed:TotalBlockingTime",
         "duration": 100,
         "entryType": "measure"
       },
@@ -5215,6 +5215,12 @@
           },
           "path": "audits[estimated-input-latency].displayValue"
         },
+        {
+          "values": {
+            "timeInMs": 116.79800000000023
+          },
+          "path": "audits[total-blocking-time].displayValue"
+        },
         {
           "values": {
             "timeInMs": 122.537
@@ -5234,6 +5240,12 @@
           "path": "audits[network-server-latency].displayValue"
         }
       ],
+      "lighthouse-core/audits/metrics/total-blocking-time.js | title": [
+        "audits[total-blocking-time].title"
+      ],
+      "lighthouse-core/audits/metrics/total-blocking-time.js | description": [
+        "audits[total-blocking-time].description"
+      ],
       "lighthouse-core/audits/metrics/max-potential-fid.js | title": [
         "audits[max-potential-fid].title"
       ],