Repository: GoogleChrome/lighthouse
Pull request #6163 (2018-10-03 03:14:25)

report: reframe Fast 3G as Slow 4G
Good summary copied from @paulirish below:
> The goal of a good mobile throttling preset is never to accurately reflect the conditions of a cellular telecommunications system. It's to reflect the conditions that real users experience.

> The preset here attempts to capture latency/throughput of ~80 percentile observed connection data from the wild. As people globally acquire faster connections, this preset should be updated to keep pace.

> Right now, we have no changes to make in the latency/throughput numbers.

> However given the recent adoption of 4G, this preset actually now captures a 4G experience moreso than a 3G experience.

--------------------------------------------


We don't actually reference 3G in *that* many places, I think this effort will be mostly about docs and how we talk to folks.

The biggest change I'm not 100% sure on how to navigate is the PWA audit and our docs linking to WPT - Fast 3G preset.

re: PWA check
I took the approach of just saying "mobile networks" and "cellular networks" since that is in fact the motivation of the check. The checklist does specifically call out "first visit on a simulated 3G network." though, so not sure if we should update it there or what :)



Patch:
diff --git a/.npmignore b/.npmignore
index 6c3e65d98..22c60bf59 100644
--- a/.npmignore
+++ b/.npmignore
@@ -15,8 +15,9 @@ lighthouse-extension/
 lighthouse-cli/results/
 lighthouse-logger/
 
-# keep the chrome launcher script
+# keep a few scripts useful for CI
 !lighthouse-core/scripts/manual-chrome-launcher.js
+!lighthouse-core/scripts/download-chrome.sh
 
 # keep smokehouse tests, etc
 !lighthouse-cli/test
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1643baa6d..0d3bda049 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -54,7 +54,7 @@ If you are adding an image to a readme use the absolute path to the image for th
 
 If you are updating an image that already exists: commit it, then update the readme to point the image with that new commits hash absolute url.
 
-## description guidelines
+## Description Guidelines
 
 Keep the `description` of an audit as short as possible. When a reference doc for the audit exists on
 developers.google.com/web, the `description` should only explain *why* the user should care
diff --git a/docs/throttling.md b/docs/throttling.md
index 104f7df35..a09033894 100644
--- a/docs/throttling.md
+++ b/docs/throttling.md
@@ -1,13 +1,13 @@
 
-## The 3G network throttling preset
+## The mobile network throttling preset
 
-This is the standard recommendation for 3G throttling:
+This is the standard recommendation for mobile throttling:
 
 - Latency: 150ms
 - Throughput: 1.6Mbps down / 750 Kbps up.
 - Packet loss: none.
 
-These exact figures are used as the [WebPageTest "Mobile 3G - Fast" preset](https://github.com/WPO-Foundation/webpagetest/blob/master/www/settings/connectivity.ini.sample) and [Lighthouse's throttling default](https://github.com/GoogleChrome/lighthouse/blob/8f500e00243e07ef0a80b39334bedcc8ddc8d3d0/lighthouse-core/config/constants.js#L19-L26).
+These exact figures are used as [Lighthouse's throttling default](https://github.com/GoogleChrome/lighthouse/blob/8f500e00243e07ef0a80b39334bedcc8ddc8d3d0/lighthouse-core/config/constants.js#L19-L26) and represent roughly the bottom 25% of 4G connections and top 25% of 3G connections. They are identical to the [WebPageTest "Mobile 3G - Fast" preset](https://github.com/WPO-Foundation/webpagetest/blob/master/www/settings/connectivity.ini.sample) and, due to a lower latency, slightly faster for some pages than the [WebPageTest "4G" preset](https://github.com/WPO-Foundation/webpagetest/blob/master/www/settings/connectivity.ini.sample).
 
 ## Throttling basics
 
diff --git a/lighthouse-core/audits/byte-efficiency/offscreen-images.js b/lighthouse-core/audits/byte-efficiency/offscreen-images.js
index fca48e3ad..0bd360ddf 100644
--- a/lighthouse-core/audits/byte-efficiency/offscreen-images.js
+++ b/lighthouse-core/audits/byte-efficiency/offscreen-images.js
@@ -165,7 +165,7 @@ class OffscreenImages extends ByteEfficiencyAudit {
    * @param {LH.Audit.Context} context
    * @return {Promise<ByteEfficiencyAudit.ByteEfficiencyProduct>}
    */
-  static audit_(artifacts, networkRecords, context) {
+  static async audit_(artifacts, networkRecords, context) {
     const images = artifacts.ImageUsage;
     const viewportDimensions = artifacts.ViewportDimensions;
     const trace = artifacts.traces[ByteEfficiencyAudit.DEFAULT_PASS];
@@ -195,29 +195,43 @@ class OffscreenImages extends ByteEfficiencyAudit {
     }, /** @type {Map<string, WasteResult>} */ (new Map()));
 
     const settings = context.settings;
-    return artifacts.requestInteractive({trace, devtoolsLog, settings}).then(interactive => {
-      const unfilteredResults = Array.from(resultsMap.values());
+
+    let items;
+    const unfilteredResults = Array.from(resultsMap.values());
+    // get the interactive time or fallback to getting the end of trace time
+    try {
+      const interactive = await artifacts.requestInteractive({trace, devtoolsLog, settings});
+
+      // use interactive to generate items
       const lanternInteractive = /** @type {LH.Artifacts.LanternMetric} */ (interactive);
       // Filter out images that were loaded after all CPU activity
-      const items = context.settings.throttlingMethod === 'simulate' ?
+      items = context.settings.throttlingMethod === 'simulate' ?
         OffscreenImages.filterLanternResults(unfilteredResults, lanternInteractive) :
         // @ts-ignore - .timestamp will exist if throttlingMethod isn't lantern
         OffscreenImages.filterObservedResults(unfilteredResults, interactive.timestamp);
+    } catch (err) {
+      // if the error is during a Lantern run, end of trace may also be inaccurate, so rethrow
+      if (context.settings.throttlingMethod === 'simulate') {
+        throw err;
+      }
+      // use end of trace as a substitute for finding interactive time
+      items = OffscreenImages.filterObservedResults(unfilteredResults,
+        await artifacts.requestTraceOfTab(trace).then(tot => tot.timestamps.traceEnd));
+    }
 
-      /** @type {LH.Result.Audit.OpportunityDetails['headings']} */
-      const headings = [
-        {key: 'url', valueType: 'thumbnail', label: ''},
-        {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
-        {key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnSize)},
-        {key: 'wastedBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnWastedBytes)},
-      ];
-
-      return {
-        warnings,
-        items,
-        headings,
-      };
-    });
+    /** @type {LH.Result.Audit.OpportunityDetails['headings']} */
+    const headings = [
+      {key: 'url', valueType: 'thumbnail', label: ''},
+      {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
+      {key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnSize)},
+      {key: 'wastedBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnWastedBytes)},
+    ];
+
+    return {
+      warnings,
+      items,
+      headings,
+    };
   }
 }
 
diff --git a/lighthouse-core/audits/dobetterweb/no-vulnerable-libraries.js b/lighthouse-core/audits/dobetterweb/no-vulnerable-libraries.js
index eec3543ff..cfa7fdf4b 100644
--- a/lighthouse-core/audits/dobetterweb/no-vulnerable-libraries.js
+++ b/lighthouse-core/audits/dobetterweb/no-vulnerable-libraries.js
@@ -153,7 +153,7 @@ class NoVulnerableLibrariesAudit extends Audit {
           vulnCount,
           detectedLib: {
             text: lib.name + '@' + version,
-            url: `https://snyk.io/vuln/npm:${lib.npmPkgName}?lh=${version}`,
+            url: `https://snyk.io/vuln/npm:${lib.npmPkgName}?lh=${version}&utm_source=lighthouse&utm_medium=ref&utm_campaign=audit`,
             type: 'link',
           },
         });
diff --git a/lighthouse-core/audits/load-fast-enough-for-pwa.js b/lighthouse-core/audits/load-fast-enough-for-pwa.js
index 62673c854..81356534e 100644
--- a/lighthouse-core/audits/load-fast-enough-for-pwa.js
+++ b/lighthouse-core/audits/load-fast-enough-for-pwa.js
@@ -13,7 +13,7 @@
 
 const isDeepEqual = require('lodash.isequal');
 const Audit = require('./audit');
-const mobile3GThrottling = require('../config/constants').throttling.mobile3G;
+const mobileThrottling = require('../config/constants').throttling.mobileSlow4G;
 
 // Maximum TTI to be considered "fast" for PWA baseline checklist
 //   https://developers.google.com/web/progressive-web-apps/checklist
@@ -26,10 +26,10 @@ class LoadFastEnough4Pwa extends Audit {
   static get meta() {
     return {
       id: 'load-fast-enough-for-pwa',
-      title: 'Page load is fast enough on 3G',
-      failureTitle: 'Page load is not fast enough on 3G',
+      title: 'Page load is fast enough on mobile networks',
+      failureTitle: 'Page load is not fast enough on mobile networks',
       description:
-        'A fast page load over a 3G network ensures a good mobile user experience. ' +
+        'A fast page load over a cellular network ensures a good mobile user experience. ' +
         '[Learn more](https://developers.google.com/web/tools/lighthouse/audits/fast-3g).',
       requiredArtifacts: ['traces', 'devtoolsLogs'],
     };
@@ -44,12 +44,12 @@ class LoadFastEnough4Pwa extends Audit {
     const trace = artifacts.traces[Audit.DEFAULT_PASS];
     const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
 
-    // If throttling was default devtools or lantern 3G throttling, then reuse the given settings
-    // Otherwise, we'll force the usage of lantern 3G.
-    const settingOverrides = {throttlingMethod: 'simulate', throttling: mobile3GThrottling};
+    // If throttling was default devtools or lantern slow 4G throttling, then reuse the given settings
+    // Otherwise, we'll force the usage of lantern slow 4G.
+    const settingOverrides = {throttlingMethod: 'simulate', throttling: mobileThrottling};
     const settings =
       context.settings.throttlingMethod !== 'provided' &&
-      isDeepEqual(context.settings.throttling, mobile3GThrottling)
+      isDeepEqual(context.settings.throttling, mobileThrottling)
         ? context.settings
         : Object.assign({}, context.settings, settingOverrides);
 
diff --git a/lighthouse-core/audits/predictive-perf.js b/lighthouse-core/audits/predictive-perf.js
index ed40192cf..129401f6e 100644
--- a/lighthouse-core/audits/predictive-perf.js
+++ b/lighthouse-core/audits/predictive-perf.js
@@ -23,7 +23,7 @@ class PredictivePerf extends Audit {
       title: 'Predicted Performance (beta)',
       description:
         'Predicted performance evaluates how your site will perform under ' +
-        'a 3G connection on a mobile device.',
+        'a cellular connection on a mobile device.',
       scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
       requiredArtifacts: ['traces', 'devtoolsLogs'],
     };
diff --git a/lighthouse-core/config/constants.js b/lighthouse-core/config/constants.js
index 5e2ec4cb4..0fcfdc60b 100644
--- a/lighthouse-core/config/constants.js
+++ b/lighthouse-core/config/constants.js
@@ -16,7 +16,7 @@ const DEVTOOLS_THROUGHPUT_ADJUSTMENT_FACTOR = 0.9;
 const throttling = {
   DEVTOOLS_RTT_ADJUSTMENT_FACTOR,
   DEVTOOLS_THROUGHPUT_ADJUSTMENT_FACTOR,
-  mobile3G: {
+  mobileSlow4G: {
     rttMs: 150,
     throughputKbps: 1.6 * 1024,
     requestLatencyMs: 150 * DEVTOOLS_RTT_ADJUSTMENT_FACTOR,
@@ -31,7 +31,7 @@ const defaultSettings = {
   output: 'json',
   maxWaitForLoad: 45 * 1000,
   throttlingMethod: 'simulate',
-  throttling: throttling.mobile3G,
+  throttling: throttling.mobileSlow4G,
   auditMode: false,
   gatherMode: false,
   disableStorageReset: false,
diff --git a/lighthouse-core/lib/dependency-graph/simulator/simulator.js b/lighthouse-core/lib/dependency-graph/simulator/simulator.js
index 18ed0f2d7..889d8ee0f 100644
--- a/lighthouse-core/lib/dependency-graph/simulator/simulator.js
+++ b/lighthouse-core/lib/dependency-graph/simulator/simulator.js
@@ -9,7 +9,7 @@ const BaseNode = require('../base-node');
 const TcpConnection = require('./tcp-connection');
 const ConnectionPool = require('./connection-pool');
 const DNSCache = require('./dns-cache');
-const mobile3G = require('../../../config/constants').throttling.mobile3G;
+const mobileSlow4G = require('../../../config/constants').throttling.mobileSlow4G;
 
 /** @typedef {BaseNode.Node} Node */
 /** @typedef {import('../network-node')} NetworkNode */
@@ -40,10 +40,10 @@ class Simulator {
     /** @type {Required<LH.Gatherer.Simulation.Options>} */
     this._options = Object.assign(
       {
-        rtt: mobile3G.rttMs,
-        throughput: mobile3G.throughputKbps * 1024,
+        rtt: mobileSlow4G.rttMs,
+        throughput: mobileSlow4G.throughputKbps * 1024,
         maximumConcurrentRequests: DEFAULT_MAXIMUM_CONCURRENT_REQUESTS,
-        cpuSlowdownMultiplier: mobile3G.cpuSlowdownMultiplier,
+        cpuSlowdownMultiplier: mobileSlow4G.cpuSlowdownMultiplier,
         layoutTaskMultiplier: DEFAULT_LAYOUT_TASK_MULTIPLIER,
         additionalRttByOrigin: new Map(),
         serverResponseTimeByOrigin: new Map(),
diff --git a/lighthouse-core/lib/emulation.js b/lighthouse-core/lib/emulation.js
index 499842c1a..b27406c6e 100644
--- a/lighthouse-core/lib/emulation.js
+++ b/lighthouse-core/lib/emulation.js
@@ -39,13 +39,13 @@ const DESKTOP_EMULATION_METRICS = {
 };
 
 const NEXUS5X_USERAGENT = {
-  userAgent: 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5 Build/MRA58N) AppleWebKit/537.36' +
-    '(KHTML, like Gecko) Chrome/71.0.3559.0 Mobile Safari/537.36',
+  // eslint-disable-next-line max-len
+  userAgent: 'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3559.0 Mobile Safari/537.36',
 };
 
 const DESKTOP_USERAGENT = {
-  userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 ' +
-    '(KHTML, like Gecko) Chrome/71.0.3559.0 Safari/537.36',
+  // eslint-disable-next-line max-len
+  userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3559.0 Safari/537.36',
 };
 
 const OFFLINE_METRICS = {
diff --git a/lighthouse-core/lib/i18n/en-US.json b/lighthouse-core/lib/i18n/en-US.json
index 00f04fd28..6b3f5e728 100644
--- a/lighthouse-core/lib/i18n/en-US.json
+++ b/lighthouse-core/lib/i18n/en-US.json
@@ -448,8 +448,8 @@
     "description": "Title of the lab data section of the Performance category. Within this section are various speed metrics which quantify the pageload performance into values presented in seconds and milliseconds. \"Lab\" is an abbreviated form of \"laboratory\", and refers to the fact that the data is from a controlled test of a website, not measurements from real users visiting that site."
   },
   "lighthouse-core/report/html/renderer/util.js | lsPerformanceCategoryDescription": {
-    "message": "[Lighthouse](https://developers.google.com/web/tools/lighthouse/) analysis of the current page on emulated 3G. Values are estimated and may vary.",
-    "description": "Explanation shown to users below performance results to inform them that the test was done with a 3G network connection and to warn them that the numbers they see will likely change slightly the next time they run Lighthouse. 'Lighthouse' becomes link text to additional documentation."
+    "message": "[Lighthouse](https://developers.google.com/web/tools/lighthouse/) analysis of the current page on an emulated mobile network. Values are estimated and may vary.",
+    "description": "Explanation shown to users below performance results to inform them that the test was done with a 4G network connection and to warn them that the numbers they see will likely change slightly the next time they run Lighthouse. 'Lighthouse' becomes link text to additional documentation."
   },
   "lighthouse-core/report/html/renderer/util.js | manualAuditsGroupTitle": {
     "message": "Additional items to manually check",
diff --git a/lighthouse-core/report/html/renderer/category-renderer.js b/lighthouse-core/report/html/renderer/category-renderer.js
index 973526c5e..5e8e2210e 100644
--- a/lighthouse-core/report/html/renderer/category-renderer.js
+++ b/lighthouse-core/report/html/renderer/category-renderer.js
@@ -164,11 +164,12 @@ class CategoryRenderer {
   renderAuditGroup(group, opts) {
     const expandable = opts.expandable;
     const groupEl = this.dom.createElement(expandable ? 'details' : 'div', 'lh-audit-group');
-    const summmaryEl = this.dom.createChildOf(groupEl, 'summary', 'lh-audit-group__summary');
-    const headerEl = this.dom.createChildOf(summmaryEl, 'div', 'lh-audit-group__header');
-    const itemCountEl = this.dom.createChildOf(summmaryEl, 'div', 'lh-audit-group__itemcount');
+    const summaryEl = this.dom.createChildOf(groupEl, 'summary');
+    const summaryInnerEl = this.dom.createChildOf(summaryEl, 'div', 'lh-audit-group__summary');
+    const headerEl = this.dom.createChildOf(summaryInnerEl, 'div', 'lh-audit-group__header');
+    const itemCountEl = this.dom.createChildOf(summaryInnerEl, 'div', 'lh-audit-group__itemcount');
     if (expandable) {
-      const chevronEl = summmaryEl.appendChild(this._createChevron());
+      const chevronEl = summaryInnerEl.appendChild(this._createChevron());
       chevronEl.title = Util.UIStrings.auditGroupExpandTooltip;
     }
 
diff --git a/lighthouse-core/report/html/renderer/report-ui-features.js b/lighthouse-core/report/html/renderer/report-ui-features.js
index 3150a9bc2..04aaddc94 100644
--- a/lighthouse-core/report/html/renderer/report-ui-features.js
+++ b/lighthouse-core/report/html/renderer/report-ui-features.js
@@ -218,8 +218,8 @@ class ReportUIFeatures {
   animateHeader() {
     const collapsedHeaderHeight = 50;
     const heightDiff = this.headerHeight - collapsedHeaderHeight + this.headerOverlap;
-    const scrollPct = Math.min(1,
-      this.latestKnownScrollY / (this.headerHeight - collapsedHeaderHeight));
+    const scrollPct = Math.max(0, Math.min(1,
+      this.latestKnownScrollY / (this.headerHeight - collapsedHeaderHeight)));
 
     const scoresContainer = /** @type {HTMLElement} */ (this.scoresWrapperBg.parentElement);
 
@@ -228,7 +228,7 @@ class ReportUIFeatures {
     this.lighthouseIcon.style.transform =
       `translate3d(calc(var(--report-width) / 2),` +
       ` calc(-100% - ${scrollPct * this.headerOverlap * -1}px), 0) scale(${1 - scrollPct})`;
-    this.lighthouseIcon.style.opacity = Math.max(0, 1 - scrollPct).toString();
+    this.lighthouseIcon.style.opacity = (1 - scrollPct).toString();
 
     // Switch up the score background & shadows
     this.scoresWrapperBg.style.opacity = (1 - scrollPct).toString();
diff --git a/lighthouse-core/report/html/renderer/util.js b/lighthouse-core/report/html/renderer/util.js
index 2b1996a1d..875a89f66 100644
--- a/lighthouse-core/report/html/renderer/util.js
+++ b/lighthouse-core/report/html/renderer/util.js
@@ -398,7 +398,7 @@ class Util {
         networkThrottling = `${Util.formatNumber(requestLatencyMs)}${NBSP}ms HTTP RTT, ` +
           `${Util.formatNumber(throttling.downloadThroughputKbps)}${NBSP}Kbps down, ` +
           `${Util.formatNumber(throttling.uploadThroughputKbps)}${NBSP}Kbps up (DevTools)`;
-        summary = 'Throttled Fast 3G network';
+        summary = 'Throttled Slow 4G network';
         break;
       }
       case 'simulate': {
@@ -406,7 +406,7 @@ class Util {
         cpuThrottling = `${Util.formatNumber(cpuSlowdownMultiplier)}x slowdown (Simulated)`;
         networkThrottling = `${Util.formatNumber(rttMs)}${NBSP}ms TCP RTT, ` +
           `${Util.formatNumber(throughputKbps)}${NBSP}Kbps throughput (Simulated)`;
-        summary = 'Simulated Fast 3G network';
+        summary = 'Simulated Slow 4G network';
         break;
       }
       default:
@@ -484,8 +484,8 @@ Util.UIStrings = {
   /** Label of value shown in the summary of critical request chains. Refers to the total amount of time (milliseconds) of the longest critical path chain/sequence of network requests. Example value: 2310 ms */
   crcLongestDurationLabel: 'Maximum critical path latency:',
 
-  /** Explanation shown to users below performance results to inform them that the test was done with a 3G network connection and to warn them that the numbers they see will likely change slightly the next time they run Lighthouse. 'Lighthouse' becomes link text to additional documentation. */
-  lsPerformanceCategoryDescription: '[Lighthouse](https://developers.google.com/web/tools/lighthouse/) analysis of the current page on emulated 3G. Values are estimated and may vary.',
+  /** Explanation shown to users below performance results to inform them that the test was done with a 4G network connection and to warn them that the numbers they see will likely change slightly the next time they run Lighthouse. 'Lighthouse' becomes link text to additional documentation. */
+  lsPerformanceCategoryDescription: '[Lighthouse](https://developers.google.com/web/tools/lighthouse/) analysis of the current page on an emulated mobile network. Values are estimated and may vary.',
   /** Title of the lab data section of the Performance category. Within this section are various speed metrics which quantify the pageload performance into values presented in seconds and milliseconds. "Lab" is an abbreviated form of "laboratory", and refers to the fact that the data is from a controlled test of a website, not measurements from real users visiting that site.  */
   labDataTitle: 'Lab Data',
 };
diff --git a/lighthouse-core/report/html/report-styles.css b/lighthouse-core/report/html/report-styles.css
index c65e8e535..26daa06c3 100644
--- a/lighthouse-core/report/html/report-styles.css
+++ b/lighthouse-core/report/html/report-styles.css
@@ -77,8 +77,6 @@
   --lh-section-vpadding: 12px;
   --chevron-size: 12px;
 
-  /* Voodoo magic here to get narrow columns. 0 doesn't size the column like our friend 1px does */
-  --bytes-col-width: 1px;
   --pass-icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><title>check</title><path fill="hsl(139, 70%, 30%)" d="M24 4C12.95 4 4 12.95 4 24c0 11.04 8.95 20 20 20 11.04 0 20-8.96 20-20 0-11.05-8.96-20-20-20zm-4 30L10 24l2.83-2.83L20 28.34l15.17-15.17L38 16 20 34z"/></svg>');
   --average-icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><title>info</title><path fill="hsl(31, 100%, 45%)" d="M24 4C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm2 30h-4V22h4v12zm0-16h-4v-4h4v4z"/></svg>');
   --fail-icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><title>warn</title><path fill="hsl(1, 73%, 45%)" d="M2 42h44L24 4 2 42zm24-6h-4v-4h4v4zm0-8h-4v-8h4v8z"/></svg>');
@@ -312,8 +310,8 @@
   background-color: #F8F9FA;
 }
 
-.lh-audit-group__summary::-webkit-details-marker,
-.lh-expandable-details__summary::-webkit-details-marker {
+.lh-audit-group > summary::-webkit-details-marker,
+.lh-expandable-details > summary::-webkit-details-marker {
   display: none;
 }
 
@@ -648,6 +646,7 @@
 }
 
 .lh-header-sticky {
+  position: -webkit-sticky;
   position: sticky;
   top: 0;
   width: 100%;
@@ -820,19 +819,6 @@
   line-height: var(--header-line-height);
 }
 
-.lh-passed-audits[open] summary.lh-passed-audits-summary {
-  margin-bottom: calc(var(--default-padding) * 2);
-}
-
-summary.lh-passed-audits-summary {
-  margin: calc(var(--default-padding) * 2) var(--default-padding);
-  margin-left: var(--default-padding);
-  margin-bottom: 0;
-  font-size: 15px;
-  display: flex;
-  align-items: center;
-}
-
 #lh-log {
   position: fixed;
   background-color: #323232;
@@ -895,6 +881,7 @@ summary.lh-passed-audits-summary {
 .lh-table thead th {
   color: var(--medium-75-gray);
   font-weight: normal;
+  word-wrap: normal;
 }
 
 .lh-table tbody tr:nth-child(even) {
@@ -930,18 +917,16 @@ summary.lh-passed-audits-summary {
 .lh-table-column--url {
   min-width: 250px;
   white-space: nowrap;
-  max-width: 0;
 }
 
-/* Keep bytes columns narrow if they follow the URL column */
+/* Keep columns narrow if they follow the URL column */
+/* 12% was determined to be a decent narrow width, but wide enough for column headings */
 .lh-table-column--url + th.lh-table-column--bytes,
 .lh-table-column--url + .lh-table-column--bytes + th.lh-table-column--bytes,
+.lh-table-column--url + .lh-table-column--ms, 
+.lh-table-column--url + .lh-table-column--ms + th.lh-table-column--bytes,
 .lh-table-column--url + .lh-table-column--bytes + th.lh-table-column--timespanMs {
-  max-width: var(--bytes-col-width);
-}
-
-.lh-table-column--code {
-  max-width: var(--url-col-max-width);
+  width: 12%;
 }
 
 .lh-text__url {
@@ -993,19 +978,19 @@ summary.lh-passed-audits-summary {
  transition: transform 300ms, stroke 300ms;
 }
 
-.lh-audit-group > .lh-audit-group__summary > .lh-chevron .lh-chevron__line-right,
-.lh-audit-group[open] > .lh-audit-group__summary > .lh-chevron .lh-chevron__line-left,
+.lh-audit-group > summary > .lh-audit-group__summary > .lh-chevron .lh-chevron__line-right,
+.lh-audit-group[open] > summary > .lh-audit-group__summary > .lh-chevron .lh-chevron__line-left,
 .lh-audit > .lh-expandable-details .lh-chevron__line-right,
 .lh-audit > .lh-expandable-details[open] .lh-chevron__line-left {
  transform: rotate(calc(var(--chevron-angle) * -1));
 }
 
-.lh-audit-group[open] > .lh-audit-group__summary > .lh-chevron .lh-chevron__line-right,
+.lh-audit-group[open] > summary > .lh-audit-group__summary > .lh-chevron .lh-chevron__line-right,
 .lh-audit > .lh-expandable-details[open] .lh-chevron__line-right {
   transform: rotate(var(--chevron-angle));
 }
 
-.lh-audit-group[open] > .lh-audit-group__summary > .lh-chevron .lh-chevron__lines,
+.lh-audit-group[open] > summary > .lh-audit-group__summary > .lh-chevron .lh-chevron__lines,
 .lh-audit > .lh-expandable-details[open] .lh-chevron__lines {
  transform: translateY(calc(var(--chevron-size) * -1));
 }
diff --git a/lighthouse-core/report/html/templates.html b/lighthouse-core/report/html/templates.html
index fd636abd4..6cb4d9979 100644
--- a/lighthouse-core/report/html/templates.html
+++ b/lighthouse-core/report/html/templates.html
@@ -56,12 +56,14 @@ limitations under the License.
 <template id="tmpl-lh-audit">
   <div class="lh-audit">
     <details class="lh-expandable-details">
-      <summary class="lh-audit__header lh-expandable-details__summary">
-        <span class="lh-audit__index"></span>
-        <span class="lh-audit__title"></span>
-        <span class="lh-audit__display-text"></span>
-        <div class="lh-audit__score-icon"></div>
-        <div class="lh-chevron-container"></div>
+      <summary>
+        <div class="lh-audit__header lh-expandable-details__summary">
+          <span class="lh-audit__index"></span>
+          <span class="lh-audit__title"></span>
+          <span class="lh-audit__display-text"></span>
+          <div class="lh-audit__score-icon"></div>
+          <div class="lh-chevron-container"></div>
+        </div>
       </summary>
       <div class="lh-audit__description"></div>
     </details>
@@ -83,21 +85,23 @@ limitations under the License.
 <template id="tmpl-lh-opportunity">
   <div class="lh-audit lh-audit--load-opportunity">
     <details class="lh-expandable-details">
-      <summary class="lh-audit__header lh-expandable-details__summary">
-        <div class="lh-load-opportunity__cols">
-          <div class="lh-load-opportunity__col lh-load-opportunity__col--one">
-            <span class="lh-audit__index"></span>
-            <div class="lh-audit__title"></div>
-          </div>
-          <div class="lh-load-opportunity__col lh-load-opportunity__col--two">
-            <div class="lh-load-opportunity__sparkline">
-              <div class="lh-sparkline"><div class="lh-sparkline__bar"></div></div>
+        <summary>
+          <div class="lh-audit__header lh-expandable-details__summary">
+            <div class="lh-load-opportunity__cols">
+              <div class="lh-load-opportunity__col lh-load-opportunity__col--one">
+                <span class="lh-audit__index"></span>
+                <div class="lh-audit__title"></div>
+              </div>
+              <div class="lh-load-opportunity__col lh-load-opportunity__col--two">
+                <div class="lh-load-opportunity__sparkline">
+                  <div class="lh-sparkline"><div class="lh-sparkline__bar"></div></div>
+                </div>
+                <div class="lh-audit__display-text"></div>
+                <div class="lh-chevron-container" title="See resources"></div>
+              </div>
             </div>
-            <div class="lh-audit__display-text"></div>
-            <div class="lh-chevron-container" title="See resources"></div>
           </div>
-        </div>
-      </summary>
+        </summary>
       <div class="lh-audit__description"></div>
     </details>
   </div>
diff --git a/lighthouse-extension/app/src/lighthouse-ext-background.js b/lighthouse-extension/app/src/lighthouse-ext-background.js
index 8556f3be2..f4b81ca51 100644
--- a/lighthouse-extension/app/src/lighthouse-ext-background.js
+++ b/lighthouse-extension/app/src/lighthouse-ext-background.js
@@ -10,14 +10,6 @@ const background = require('./lighthouse-background');
 
 const ExtensionProtocol = require('../../../lighthouse-core/gather/connections/extension');
 const log = require('lighthouse-logger');
-const assetSaver = require('../../../lighthouse-core/lib/asset-saver.js');
-const LHError = require('../../../lighthouse-core/lib/lh-error.js');
-
-/** @type {Record<'mobile'|'desktop', LH.Config.Json>} */
-const LR_PRESETS = {
-  mobile: require('../../../lighthouse-core/config/lr-mobile-config'),
-  desktop: require('../../../lighthouse-core/config/lr-desktop-config'),
-};
 
 /** @typedef {import('../../../lighthouse-core/gather/connections/connection.js')} Connection */
 
@@ -87,56 +79,6 @@ async function runLighthouseInExtension(flags, categoryIDs) {
   await new Promise(resolve => chrome.windows.create({url: blobURL}, resolve));
 }
 
-/**
- * Run lighthouse for connection and provide similar results as in CLI.
- * @param {Connection} connection
- * @param {string} url
- * @param {LH.Flags} flags Lighthouse flags, including `output`
- * @param {{lrDevice?: 'desktop'|'mobile', categoryIDs?: Array<string>, logAssets: boolean}} lrOpts Options coming from Lightrider
- * @return {Promise<string|Array<string>|void>}
- */
-async function runLighthouseInLR(connection, url, flags, {lrDevice, categoryIDs, logAssets}) {
-  // Certain fixes need to kick-in under LR, see https://github.com/GoogleChrome/lighthouse/issues/5839
-  global.isLightRider = true;
-
-  // disableStorageReset because it causes render server hang
-  flags.disableStorageReset = true;
-  flags.logLevel = flags.logLevel || 'info';
-  const config = lrDevice === 'desktop' ? LR_PRESETS.desktop : LR_PRESETS.mobile;
-  if (categoryIDs) {
-    config.settings = config.settings || {};
-    config.settings.onlyCategories = categoryIDs;
-  }
-
-  try {
-    const results = await lighthouse(url, flags, config, connection);
-    if (!results) return;
-
-    if (logAssets) {
-      await assetSaver.logAssets(results.artifacts, results.lhr.audits);
-    }
-    return results.report;
-  } catch (err) {
-    // If an error ruined the entire lighthouse run, attempt to return a meaningful error.
-    let runtimeError;
-    if (!(err instanceof LHError) || !err.lhrRuntimeError) {
-      runtimeError = {
-        code: LHError.UNKNOWN_ERROR,
-        message: `Unknown error encountered with message '${err.message}'`,
-      };
-    } else {
-      runtimeError = {
-        code: err.code,
-        message: err.friendlyMessage ?
-            `${err.friendlyMessage} (${err.message})` :
-            err.message,
-      };
-    }
-
-    return JSON.stringify({runtimeError}, null, 2);
-  }
-}
-
 /**
  * @param {string} reportHtml
  * @return {string} Blob URL of the report (or error page) HTML
@@ -241,7 +183,6 @@ if (typeof module !== 'undefined' && module.exports) {
   // Export for importing types into popup.js, require()ing into unit tests.
   module.exports = {
     runLighthouseInExtension,
-    runLighthouseInLR,
     getDefaultCategories: background.getDefaultCategories,
     isRunning,
     listenForStatus,
@@ -255,8 +196,6 @@ if (typeof window !== 'undefined') {
   // @ts-ignore
   window.runLighthouseInExtension = runLighthouseInExtension;
   // @ts-ignore
-  window.runLighthouseInLR = runLighthouseInLR;
-  // @ts-ignore
   window.getDefaultCategories = background.getDefaultCategories;
   // @ts-ignore
   window.isRunning = isRunning;
diff --git a/lighthouse-extension/app/src/lighthouse-lr-background.js b/lighthouse-extension/app/src/lighthouse-lr-background.js
new file mode 100644
index 000000000..426d42428
--- /dev/null
+++ b/lighthouse-extension/app/src/lighthouse-lr-background.js
@@ -0,0 +1,82 @@
+/**
+ * @license Copyright 2018 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 lighthouse = require('../../../lighthouse-core/index');
+
+const assetSaver = require('../../../lighthouse-core/lib/asset-saver.js');
+const LHError = require('../../../lighthouse-core/lib/lh-error.js');
+
+/** @type {Record<'mobile'|'desktop', LH.Config.Json>} */
+const LR_PRESETS = {
+  mobile: require('../../../lighthouse-core/config/lr-mobile-config'),
+  desktop: require('../../../lighthouse-core/config/lr-desktop-config'),
+};
+
+/** @typedef {import('../../../lighthouse-core/gather/connections/connection.js')} Connection */
+
+/**
+ * Run lighthouse for connection and provide similar results as in CLI.
+ * @param {Connection} connection
+ * @param {string} url
+ * @param {LH.Flags} flags Lighthouse flags, including `output`
+ * @param {{lrDevice?: 'desktop'|'mobile', categoryIDs?: Array<string>, logAssets: boolean}} lrOpts Options coming from Lightrider
+ * @return {Promise<string|Array<string>|void>}
+ */
+async function runLighthouseInLR(connection, url, flags, {lrDevice, categoryIDs, logAssets}) {
+  // Certain fixes need to kick-in under LR, see https://github.com/GoogleChrome/lighthouse/issues/5839
+  global.isLightRider = true;
+
+  // disableStorageReset because it causes render server hang
+  flags.disableStorageReset = true;
+  flags.logLevel = flags.logLevel || 'info';
+  const config = lrDevice === 'desktop' ? LR_PRESETS.desktop : LR_PRESETS.mobile;
+  if (categoryIDs) {
+    config.settings = config.settings || {};
+    config.settings.onlyCategories = categoryIDs;
+  }
+
+  try {
+    const results = await lighthouse(url, flags, config, connection);
+    if (!results) return;
+
+    if (logAssets) {
+      await assetSaver.logAssets(results.artifacts, results.lhr.audits);
+    }
+    return results.report;
+  } catch (err) {
+    // If an error ruined the entire lighthouse run, attempt to return a meaningful error.
+    let runtimeError;
+    if (!(err instanceof LHError) || !err.lhrRuntimeError) {
+      runtimeError = {
+        code: LHError.UNKNOWN_ERROR,
+        message: `Unknown error encountered with message '${err.message}'`,
+      };
+    } else {
+      runtimeError = {
+        code: err.code,
+        message: err.friendlyMessage ?
+            `${err.friendlyMessage} (${err.message})` :
+            err.message,
+      };
+    }
+
+    return JSON.stringify({runtimeError}, null, 2);
+  }
+}
+
+if (typeof module !== 'undefined' && module.exports) {
+  // Export for importing types into popup.js, require()ing into unit tests.
+  module.exports = {
+    runLighthouseInLR,
+  };
+}
+
+// Expose on window for extension, other browser-residing consumers of file.
+if (typeof window !== 'undefined') {
+  // @ts-ignore
+  window.runLighthouseInLR = runLighthouseInLR;
+}
diff --git a/lighthouse-extension/gulpfile.js b/lighthouse-extension/gulpfile.js
index 3e71daf2e..de0249793 100644
--- a/lighthouse-extension/gulpfile.js
+++ b/lighthouse-extension/gulpfile.js
@@ -34,6 +34,13 @@ const pkg = require('../package.json');
 
 const distDir = 'dist';
 
+// list of all consumers we build for (easier to understand which file is used for which)
+const CONSUMERS = {
+  DEVTOOLS: 'lighthouse-background.js',
+  EXTENSION: 'lighthouse-ext-background.js',
+  LIGHTRIDER: 'lighthouse-lr-background.js',
+};
+
 const VERSION = pkg.version;
 const COMMIT_HASH = require('child_process')
   .execSync('git rev-parse HEAD')
@@ -50,6 +57,12 @@ const gatherers = LighthouseRunner.getGathererList()
 const computedArtifacts = LighthouseRunner.getComputedGathererList()
     .map(f => '../lighthouse-core/gather/computed/' + f.replace(/\.js$/, ''));
 
+const locales = fs.readdirSync('../lighthouse-core/lib/i18n/locales/')
+    .map(f => require.resolve(`../lighthouse-core/lib/i18n/locales/${f}`));
+
+const isDevtools = file => file.endsWith(CONSUMERS.DEVTOOLS);
+const isExtension = file => file.endsWith(CONSUMERS.EXTENSION);
+
 gulp.task('extras', () => {
   return gulp.src([
     'app/*.*',
@@ -96,7 +109,7 @@ gulp.task('chromeManifest', () => {
   const manifestOpts = {
     buildnumber: false,
     background: {
-      target: 'scripts/lighthouse-ext-background.js',
+      target: `scripts/${CONSUMERS.EXTENSION}`,
     },
   };
   return gulp.src('app/manifest.json')
@@ -114,12 +127,10 @@ function applyBrowserifyTransforms(bundle) {
 }
 
 gulp.task('browserify-lighthouse', () => {
-  return gulp.src([
-    'app/src/lighthouse-background.js',
-    'app/src/lighthouse-ext-background.js',
-  ], {read: false})
+  const consumerSources = Object.values(CONSUMERS).map(consumer => `app/src/${consumer}`);
+  return gulp.src(consumerSources, {read: false})
     .pipe(tap(file => {
-      let bundle = browserify(file.path); // , {debug: true}); // for sourcemaps
+      let bundle = browserify(file.path, {debug: true}); // for sourcemaps
       bundle = applyBrowserifyTransforms(bundle);
 
       // scripts will need some additional transforms, ignores and requires…
@@ -135,10 +146,14 @@ gulp.task('browserify-lighthouse', () => {
       bundle.ignore(require.resolve('../lighthouse-core/gather/connections/cri.js'));
 
       // Prevent the DevTools background script from getting the stringified HTML.
-      if (/lighthouse-background/.test(file.path)) {
+      if (isDevtools(file.path)) {
         bundle.ignore(require.resolve('../lighthouse-core/report/html/html-report-assets.js'));
       }
 
+      if (isDevtools(file.path) || isExtension(file.path)) {
+        bundle.ignore(locales);
+      }
+
       // Expose the audits, gatherers, and computed artifacts so they can be dynamically loaded.
       const corePath = '../lighthouse-core/';
       const driverPath = `${corePath}gather/`;
@@ -197,9 +212,8 @@ gulp.task('compilejs', () => {
     // sourceMaps: 'both'
   };
 
-  return gulp.src([
-    'dist/scripts/lighthouse-background.js',
-    'dist/scripts/lighthouse-ext-background.js'])
+  const compiledSources = Object.values(CONSUMERS).map(consumer => `dist/scripts/${consumer}`);
+  return gulp.src(compiledSources)
     .pipe(tap(file => {
       const minified = babel.transform(file.contents.toString(), opts).code;
       file.contents = new Buffer(minified);
diff --git a/readme.md b/readme.md
index c738e1b90..0c7532d76 100644
--- a/readme.md
+++ b/readme.md
@@ -304,7 +304,7 @@ Yes! Details in [Lighthouse configuration](./docs/configuration.md).
 ### How does Lighthouse use network throttling, and how can I make it better?
 
 Good question. Network and CPU throttling are applied by default in a Lighthouse run. The network
-attempts to emulate 3G and the CPU is slowed down 4x from your machine's default speed. If you
+attempts to emulate slow 4G and the CPU is slowed down 4x from your machine's default speed. If you
 prefer to run Lighthouse without throttling, you'll have to use the CLI and disable it with the
 `--throttling.*` flags mentioned above.
 
@@ -338,4 +338,4 @@ See [Contributing](./CONTRIBUTING.md) to get started.
   <br>
   <b>Lighthouse</b>, ˈlītˌhous (n): a 
tower or other structure
 tool containing a beacon light
   to warn or guide 
ships at sea
 developers.
-</p>
\ No newline at end of file
+</p>


Test patch:
diff --git a/lighthouse-core/test/audits/byte-efficiency/offscreen-images-test.js b/lighthouse-core/test/audits/byte-efficiency/offscreen-images-test.js
index 4999fa917..e101adeea 100644
--- a/lighthouse-core/test/audits/byte-efficiency/offscreen-images-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/offscreen-images-test.js
@@ -10,6 +10,7 @@ const UnusedImages =
 const NetworkNode = require('../../../lib/dependency-graph/network-node');
 const CPUNode = require('../../../lib/dependency-graph/cpu-node');
 const assert = require('assert');
+const LHError = require('../../../lib/lh-error');
 
 /* eslint-env jest */
 function generateRecord(resourceSizeInKb, startTime = 0, mimeType = 'image/png') {
@@ -50,6 +51,20 @@ function generateInteractiveFunc(desiredTimeInSeconds) {
   });
 }
 
+function generateInteractiveFuncError() {
+  return () => Promise.reject(
+    new LHError(LHError.errors.NO_TTI_NETWORK_IDLE_PERIOD)
+  );
+}
+
+function generateTraceOfTab(desiredTimeInSeconds) {
+  return () => Promise.resolve({
+    timestamps: {
+      traceEnd: desiredTimeInSeconds * 1000000,
+    },
+  });
+}
+
 describe('OffscreenImages audit', () => {
   let context;
   const DEFAULT_DIMENSIONS = {innerWidth: 1920, innerHeight: 1080};
@@ -162,6 +177,38 @@ describe('OffscreenImages audit', () => {
     });
   });
 
+  it('disregards images loaded after Trace End when interactive throws error', () => {
+    return UnusedImages.audit_({
+      ViewportDimensions: DEFAULT_DIMENSIONS,
+      ImageUsage: [
+        // offscreen to the right
+        generateImage(generateSize(200, 200), [3000, 0], generateRecord(100, 3)),
+      ],
+      traces: {},
+      devtoolsLogs: {},
+      requestInteractive: generateInteractiveFuncError(),
+      requestTraceOfTab: generateTraceOfTab(2),
+    }, [], context, [], context).then(auditResult => {
+      assert.equal(auditResult.items.length, 0);
+    });
+  });
+
+  it('finds images loaded before Trace End when TTI when interactive throws error', () => {
+    return UnusedImages.audit_({
+      ViewportDimensions: DEFAULT_DIMENSIONS,
+      ImageUsage: [
+        // offscreen to the right
+        generateImage(generateSize(100, 100), [0, 2000], generateRecord(100)),
+      ],
+      traces: {},
+      devtoolsLogs: {},
+      requestInteractive: generateInteractiveFuncError(),
+      requestTraceOfTab: generateTraceOfTab(2),
+    }, [], context, [], context).then(auditResult => {
+      assert.equal(auditResult.items.length, 1);
+    });
+  });
+
   it('disregards images loaded after last long task (Lantern)', () => {
     context = {settings: {throttlingMethod: 'simulate'}};
     const recordA = {url: 'a', resourceSize: 100 * 1024, requestId: 'a'};
@@ -190,4 +237,75 @@ describe('OffscreenImages audit', () => {
       assert.equal(auditResult.items[0].url, 'a');
     });
   });
+
+  it('finds images loaded before last long task (Lantern)', () => {
+    context = {settings: {throttlingMethod: 'simulate'}};
+    const recordA = {url: 'a', resourceSize: 100 * 1024, requestId: 'a'};
+    const recordB = {url: 'b', resourceSize: 100 * 1024, requestId: 'b'};
+
+    const networkA = new NetworkNode(recordA);
+    const networkB = new NetworkNode(recordB);
+    const cpu = new CPUNode({}, []);
+    const timings = new Map([
+      [networkA, {startTime: 1000}],
+      [networkB, {startTime: 1500}],
+      [cpu, {startTime: 1975, endTime: 2025, duration: 50}],
+    ]);
+
+    return UnusedImages.audit_({
+      ViewportDimensions: DEFAULT_DIMENSIONS,
+      ImageUsage: [
+        generateImage(generateSize(0, 0), [0, 0], recordA, recordA.url),
+        generateImage(generateSize(200, 200), [3000, 0], recordB, recordB.url),
+      ],
+      traces: {},
+      devtoolsLogs: {},
+      requestInteractive: async () => ({pessimisticEstimate: {nodeTimings: timings}}),
+      requestTraceOfTab: generateTraceOfTab(2),
+    }, [], context, [], context).then(auditResult => {
+      assert.equal(auditResult.items.length, 2);
+      assert.equal(auditResult.items[0].url, 'a');
+      assert.equal(auditResult.items[1].url, 'b');
+    });
+  });
+
+  it('rethrow error when interactive throws error in Lantern', async () => {
+    context = {settings: {throttlingMethod: 'simulate'}};
+    try {
+      await UnusedImages.audit_({
+        ViewportDimensions: DEFAULT_DIMENSIONS,
+        ImageUsage: [
+          generateImage(generateSize(0, 0), [0, 0], generateRecord(100, 3), 'a'),
+          generateImage(generateSize(200, 200), [3000, 0], generateRecord(100, 4), 'b'),
+        ],
+        traces: {},
+        devtoolsLogs: {},
+        requestInteractive: generateInteractiveFuncError(),
+        requestTraceOfTab: generateTraceOfTab(2),
+      }, [], context, [], context);
+    } catch (err) {
+      return;
+    }
+    assert.ok(false);
+  });
+
+  it('finds images loaded before Trace End when interactive throws error (Lantern)', async () => {
+    context = {settings: {throttlingMethod: 'simulate'}};
+    try {
+      await UnusedImages.audit_({
+        ViewportDimensions: DEFAULT_DIMENSIONS,
+        ImageUsage: [
+          generateImage(generateSize(0, 0), [0, 0], generateRecord(100, 1), 'a'),
+          generateImage(generateSize(200, 200), [3000, 0], generateRecord(100, 4), 'b'),
+        ],
+        traces: {},
+        devtoolsLogs: {},
+        requestInteractive: generateInteractiveFuncError(),
+        requestTraceOfTab: generateTraceOfTab(2),
+      }, [], context, [], context);
+    } catch (err) {
+      return;
+    }
+    assert.ok(false);
+  });
 });
diff --git a/lighthouse-core/test/audits/byte-efficiency/render-blocking-resources-test.js b/lighthouse-core/test/audits/byte-efficiency/render-blocking-resources-test.js
index e1a71999a..1518e7f46 100644
--- a/lighthouse-core/test/audits/byte-efficiency/render-blocking-resources-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/render-blocking-resources-test.js
@@ -7,7 +7,7 @@
 
 const RenderBlockingResourcesAudit = require('../../../audits/byte-efficiency/render-blocking-resources.js'); // eslint-disable-line max-len
 
-const mobile3G = require('../../../config/constants').throttling.mobile3G;
+const mobileSlow4G = require('../../../config/constants').throttling.mobileSlow4G;
 const Runner = require('../../../runner');
 const NetworkNode = require('../../../lib/dependency-graph/network-node');
 const CPUNode = require('../../../lib/dependency-graph/cpu-node');
@@ -36,7 +36,7 @@ describe('Render blocking resources audit', () => {
       Runner.instantiateComputedArtifacts()
     );
 
-    const settings = {throttlingMethod: 'simulate', throttling: mobile3G};
+    const settings = {throttlingMethod: 'simulate', throttling: mobileSlow4G};
     const result = await RenderBlockingResourcesAudit.audit(artifacts, {settings});
     assert.equal(result.score, 1);
     assert.equal(result.rawValue, 0);
diff --git a/lighthouse-core/test/audits/byte-efficiency/total-byte-weight-test.js b/lighthouse-core/test/audits/byte-efficiency/total-byte-weight-test.js
index cf6f14f3a..71ff3c152 100644
--- a/lighthouse-core/test/audits/byte-efficiency/total-byte-weight-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/total-byte-weight-test.js
@@ -9,6 +9,8 @@ const TotalByteWeight = require('../../../audits/byte-efficiency/total-byte-weig
 const assert = require('assert');
 const URL = require('url').URL;
 const options = TotalByteWeight.defaultOptions;
+const Runner = require('../../../runner.js');
+const networkRecordsToDevtoolsLog = require('../../network-records-to-devtools-log.js');
 
 /* eslint-env jest */
 
@@ -32,10 +34,9 @@ function generateArtifacts(records) {
     records = records.map(args => generateRequest(...args));
   }
 
-  return {
-    devtoolsLogs: {defaultPass: []},
-    requestNetworkRecords: () => Promise.resolve(records),
-  };
+  return Object.assign(Runner.instantiateComputedArtifacts(), {
+    devtoolsLogs: {defaultPass: networkRecordsToDevtoolsLog(records)},
+  });
 }
 
 describe('Total byte weight audit', () => {
diff --git a/lighthouse-core/test/audits/byte-efficiency/unused-css-rules-test.js b/lighthouse-core/test/audits/byte-efficiency/unused-css-rules-test.js
index 4aa9bcd44..4ee98c8f4 100644
--- a/lighthouse-core/test/audits/byte-efficiency/unused-css-rules-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/unused-css-rules-test.js
@@ -7,6 +7,8 @@
 
 const UnusedCSSAudit = require('../../../audits/byte-efficiency/unused-css-rules.js');
 const assert = require('assert');
+const Runner = require('../../../runner.js');
+const networkRecordsToDevtoolsLog = require('../../network-records-to-devtools-log.js');
 
 /* eslint-env jest */
 
@@ -117,33 +119,32 @@ describe('Best Practices: unused css rules audit', () => {
   });
 
   describe('#audit', () => {
-    const devtoolsLogs = {defaultPass: []};
-    const requestNetworkRecords = () => {
-      return Promise.resolve([
-        {
-          url: 'file://a.css',
-          transferSize: 10 * 1024,
-          resourceType: 'Stylesheet',
-        },
-      ]);
-    };
+    const networkRecords = [
+      {
+        url: 'file://a.css',
+        transferSize: 10 * 1024,
+        resourceType: 'Stylesheet',
+      },
+    ];
+
+    function getArtifacts({CSSUsage}) {
+      return Object.assign(Runner.instantiateComputedArtifacts(), {
+        devtoolsLogs: {defaultPass: networkRecordsToDevtoolsLog(networkRecords)},
+        URL: {finalUrl: ''},
+        CSSUsage,
+      });
+    }
 
     it('ignores missing stylesheets', () => {
-      return UnusedCSSAudit.audit_({
-        devtoolsLogs,
-        requestNetworkRecords,
-        URL: {finalUrl: ''},
+      return UnusedCSSAudit.audit_(getArtifacts({
         CSSUsage: {rules: [{styleSheetId: 'a', used: false}], stylesheets: []},
-      }).then(result => {
+      })).then(result => {
         assert.equal(result.items.length, 0);
       });
     });
 
     it('ignores stylesheets that are 100% used', () => {
-      return UnusedCSSAudit.audit_({
-        devtoolsLogs,
-        requestNetworkRecords,
-        URL: {finalUrl: ''},
+      return UnusedCSSAudit.audit_(getArtifacts({
         CSSUsage: {rules: [
           {styleSheetId: 'a', used: true},
           {styleSheetId: 'a', used: true},
@@ -158,16 +159,13 @@ describe('Best Practices: unused css rules audit', () => {
             content: '.my.favorite.selector { rule: content; }',
           },
         ]},
-      }).then(result => {
+      })).then(result => {
         assert.equal(result.items.length, 0);
       });
     });
 
     it('fails when lots of rules are unused', () => {
-      return UnusedCSSAudit.audit_({
-        devtoolsLogs,
-        requestNetworkRecords,
-        URL: {finalUrl: ''},
+      return UnusedCSSAudit.audit_(getArtifacts({
         CSSUsage: {rules: [
           {styleSheetId: 'a', used: true, startOffset: 0, endOffset: 11}, // 44 * 1 / 4
           {styleSheetId: 'b', used: true, startOffset: 0, endOffset: 6000}, // 4000 * 3 / 2
@@ -185,7 +183,7 @@ describe('Best Practices: unused css rules audit', () => {
             content: `${generate('123', 450)}`, // will be filtered out
           },
         ]},
-      }).then(result => {
+      })).then(result => {
         assert.equal(result.items.length, 2);
         assert.equal(result.items[0].totalBytes, 10 * 1024);
         assert.equal(result.items[1].totalBytes, 6000);
@@ -195,10 +193,7 @@ describe('Best Practices: unused css rules audit', () => {
     });
 
     it('does not include empty or small sheets', () => {
-      return UnusedCSSAudit.audit_({
-        devtoolsLogs,
-        requestNetworkRecords,
-        URL: {finalUrl: ''},
+      return UnusedCSSAudit.audit_(getArtifacts({
         CSSUsage: {rules: [
           {styleSheetId: 'a', used: true, startOffset: 0, endOffset: 8000}, // 4000 * 3 / 2
           {styleSheetId: 'b', used: true, startOffset: 0, endOffset: 500}, // 500 * 3 / 3
@@ -224,7 +219,7 @@ describe('Best Practices: unused css rules audit', () => {
             content: '       ',
           },
         ]},
-      }).then(result => {
+      })).then(result => {
         assert.equal(result.items.length, 1);
         assert.equal(Math.floor(result.items[0].wastedPercent), 33);
       });
diff --git a/lighthouse-core/test/audits/byte-efficiency/uses-long-cache-ttl-test.js b/lighthouse-core/test/audits/byte-efficiency/uses-long-cache-ttl-test.js
index d49bae881..c80cc6c77 100644
--- a/lighthouse-core/test/audits/byte-efficiency/uses-long-cache-ttl-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/uses-long-cache-ttl-test.js
@@ -9,6 +9,8 @@ const CacheHeadersAudit = require('../../../audits/byte-efficiency/uses-long-cac
 const assert = require('assert');
 const NetworkRequest = require('../../../lib/network-request');
 const options = CacheHeadersAudit.defaultOptions;
+const Runner = require('../../../runner.js');
+const networkRecordsToDevtoolsLog = require('../../network-records-to-devtools-log.js');
 
 /* eslint-env jest */
 
@@ -28,20 +30,20 @@ function networkRecord(options = {}) {
 }
 
 describe('Cache headers audit', () => {
-  let artifacts;
-  let networkRecords;
+  function getArtifacts(networkRecords) {
+    const devtoolLogs = networkRecordsToDevtoolsLog(networkRecords);
 
-  beforeEach(() => {
-    artifacts = {
-      devtoolsLogs: {},
-      requestNetworkRecords: () => Promise.resolve(networkRecords),
+    return Object.assign(Runner.instantiateComputedArtifacts(), {
+      devtoolsLogs: {
+        [CacheHeadersAudit.DEFAULT_PASS]: devtoolLogs,
+      },
       requestNetworkThroughput: () => Promise.resolve(1000),
-    };
-  });
+    });
+  }
 
   it('detects missing cache headers', () => {
-    networkRecords = [networkRecord()];
-    return CacheHeadersAudit.audit(artifacts, {options}).then(result => {
+    const networkRecords = [networkRecord()];
+    return CacheHeadersAudit.audit(getArtifacts(networkRecords), {options}).then(result => {
       const items = result.extendedInfo.value.results;
       assert.equal(items.length, 1);
       assert.equal(items[0].cacheLifetimeMs, 0);
@@ -51,14 +53,14 @@ describe('Cache headers audit', () => {
   });
 
   it('detects low value max-age headers', () => {
-    networkRecords = [
+    const networkRecords = [
       networkRecord({headers: {'cache-control': 'max-age=3600'}}), // an hour
       networkRecord({headers: {'cache-control': 'max-age=3600'}, transferSize: 100000}), // an hour
       networkRecord({headers: {'cache-control': 'max-age=86400'}}), // a day
       networkRecord({headers: {'cache-control': 'max-age=31536000'}}), // a year
     ];
 
-    return CacheHeadersAudit.audit(artifacts, {options}).then(result => {
+    return CacheHeadersAudit.audit(getArtifacts(networkRecords), {options}).then(result => {
       const items = result.details.items;
       assert.equal(items.length, 3);
       assert.equal(items[0].cacheLifetimeMs, 3600 * 1000);
@@ -76,14 +78,14 @@ describe('Cache headers audit', () => {
     const expiresIn = seconds => new Date(Date.now() + seconds * 1000).toGMTString();
     const closeEnough = (actual, exp) => assert.ok(Math.abs(actual - exp) <= 1, 'invalid expires');
 
-    networkRecords = [
+    const networkRecords = [
       networkRecord({headers: {expires: expiresIn(86400 * 365)}}), // a year
       networkRecord({headers: {expires: expiresIn(86400 * 90)}}), // 3 months
       networkRecord({headers: {expires: expiresIn(86400)}}), // a day
       networkRecord({headers: {expires: expiresIn(3600)}}), // an hour
     ];
 
-    return CacheHeadersAudit.audit(artifacts, {options}).then(result => {
+    return CacheHeadersAudit.audit(getArtifacts(networkRecords), {options}).then(result => {
       const items = result.extendedInfo.value.results;
       assert.equal(items.length, 3);
       closeEnough(items[0].cacheLifetimeMs, 3600 * 1000);
@@ -98,7 +100,7 @@ describe('Cache headers audit', () => {
   it('respects expires/cache-control priority', () => {
     const expiresIn = seconds => new Date(Date.now() + seconds * 1000).toGMTString();
 
-    networkRecords = [
+    const networkRecords = [
       networkRecord({headers: {
         'cache-control': 'must-revalidate,max-age=3600',
         'expires': expiresIn(86400),
@@ -109,7 +111,7 @@ describe('Cache headers audit', () => {
       }}),
     ];
 
-    return CacheHeadersAudit.audit(artifacts, {options}).then(result => {
+    return CacheHeadersAudit.audit(getArtifacts(networkRecords), {options}).then(result => {
       const items = result.extendedInfo.value.results;
       assert.equal(items.length, 2);
       assert.ok(Math.abs(items[0].cacheLifetimeMs - 3600 * 1000) <= 1, 'invalid expires parsing');
@@ -120,7 +122,7 @@ describe('Cache headers audit', () => {
   });
 
   it('respects multiple cache-control headers', () => {
-    networkRecords = [
+    const networkRecords = [
       networkRecord({headers: {
         'cache-control': 'max-age=31536000, public',
         'Cache-control': 'no-transform',
@@ -132,26 +134,26 @@ describe('Cache headers audit', () => {
       }}),
     ];
 
-    return CacheHeadersAudit.audit(artifacts, {options}).then(result => {
+    return CacheHeadersAudit.audit(getArtifacts(networkRecords), {options}).then(result => {
       const items = result.extendedInfo.value.results;
       assert.equal(items.length, 1);
     });
   });
 
   it('catches records with Etags', () => {
-    networkRecords = [
+    const networkRecords = [
       networkRecord({headers: {etag: 'md5hashhere'}}),
       networkRecord({headers: {'etag': 'md5hashhere', 'cache-control': 'max-age=60'}}),
     ];
 
-    return CacheHeadersAudit.audit(artifacts, {options}).then(result => {
+    return CacheHeadersAudit.audit(getArtifacts(networkRecords), {options}).then(result => {
       const items = result.extendedInfo.value.results;
       assert.equal(items.length, 2);
     });
   });
 
   it('ignores explicit no-cache policies', () => {
-    networkRecords = [
+    const networkRecords = [
       networkRecord({headers: {expires: '-1'}}),
       networkRecord({headers: {'cache-control': 'no-store'}}),
       networkRecord({headers: {'cache-control': 'no-cache'}}),
@@ -159,7 +161,7 @@ describe('Cache headers audit', () => {
       networkRecord({headers: {pragma: 'no-cache'}}),
     ];
 
-    return CacheHeadersAudit.audit(artifacts, {options}).then(result => {
+    return CacheHeadersAudit.audit(getArtifacts(networkRecords), {options}).then(result => {
       const items = result.extendedInfo.value.results;
       assert.equal(result.score, 1);
       assert.equal(items.length, 0);
@@ -167,14 +169,14 @@ describe('Cache headers audit', () => {
   });
 
   it('ignores potentially uncacheable records', () => {
-    networkRecords = [
+    const networkRecords = [
       networkRecord({statusCode: 500}),
       networkRecord({url: 'https://example.com/dynamic.js?userId=crazy', transferSize: 10}),
       networkRecord({url: 'data:image/jpeg;base64,what'}),
       networkRecord({resourceType: NetworkRequest.TYPES.XHR}),
     ];
 
-    return CacheHeadersAudit.audit(artifacts, {options}).then(result => {
+    return CacheHeadersAudit.audit(getArtifacts(networkRecords), {options}).then(result => {
       assert.equal(result.score, 1);
       const items = result.extendedInfo.value.results;
       assert.equal(items.length, 1);
diff --git a/lighthouse-core/test/audits/critical-request-chains-test.js b/lighthouse-core/test/audits/critical-request-chains-test.js
index a7ce7ce9c..f5b3bacf2 100644
--- a/lighthouse-core/test/audits/critical-request-chains-test.js
+++ b/lighthouse-core/test/audits/critical-request-chains-test.js
@@ -7,6 +7,7 @@
 
 /* eslint-env jest */
 
+const Runner = require('../../runner.js');
 const CriticalRequestChains = require('../../audits/critical-request-chains.js');
 const assert = require('assert');
 
@@ -70,17 +71,14 @@ const PASSING_REQUEST_CHAIN_2 = {
 const EMPTY_REQUEST_CHAIN = {};
 
 const mockArtifacts = (mockChain) => {
-  return {
+  return Object.assign(Runner.instantiateComputedArtifacts(), {
     devtoolsLogs: {
       [CriticalRequestChains.DEFAULT_PASS]: [],
     },
-    requestNetworkRecords: () => {
-      return Promise.resolve([]);
-    },
     requestCriticalRequestChains: function() {
       return Promise.resolve(mockChain);
     },
-  };
+  });
 };
 
 describe('Performance: critical-request-chains audit', () => {
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 c225d7272..6c9151c61 100644
--- a/lighthouse-core/test/audits/dobetterweb/no-vulnerable-libraries-test.js
+++ b/lighthouse-core/test/audits/dobetterweb/no-vulnerable-libraries-test.js
@@ -40,7 +40,7 @@ describe('Avoids front-end JavaScript libraries with known vulnerabilities', ()
     assert.equal(auditResult.details.items[0].highestSeverity, 'High');
     assert.equal(auditResult.details.items[0].detectedLib.type, 'link');
     assert.equal(auditResult.details.items[0].detectedLib.text, 'angular@1.1.4');
-    assert.equal(auditResult.details.items[0].detectedLib.url, 'https://snyk.io/vuln/npm:angular?lh=1.1.4');
+    assert.equal(auditResult.details.items[0].detectedLib.url, 'https://snyk.io/vuln/npm:angular?lh=1.1.4&utm_source=lighthouse&utm_medium=ref&utm_campaign=audit');
   });
 
   it('handles ill-specified versions', () => {
diff --git a/lighthouse-core/test/audits/dobetterweb/uses-http2-test.js b/lighthouse-core/test/audits/dobetterweb/uses-http2-test.js
index b20ecb67e..7d27a18b9 100644
--- a/lighthouse-core/test/audits/dobetterweb/uses-http2-test.js
+++ b/lighthouse-core/test/audits/dobetterweb/uses-http2-test.js
@@ -5,8 +5,10 @@
  */
 'use strict';
 
+const Runner = require('../../../runner.js');
 const UsesHTTP2Audit = require('../../../audits/dobetterweb/uses-http2.js');
 const assert = require('assert');
+const networkRecordsToDevtoolsLog = require('../../network-records-to-devtools-log.js');
 
 const URL = 'https://webtide.com/http2-push-demo/';
 const networkRecords = require('../../fixtures/networkRecords-mix.json');
@@ -14,12 +16,20 @@ const networkRecords = require('../../fixtures/networkRecords-mix.json');
 /* eslint-env jest */
 
 describe('Resources are fetched over http/2', () => {
+  function getArtifacts(networkRecords, finalUrl) {
+    // networkRecords-mix.json is an old network request format, so don't verify round-trip.
+    const devtoolsLog = networkRecordsToDevtoolsLog(networkRecords, {skipVerification: true});
+
+    return Object.assign(Runner.instantiateComputedArtifacts(), {
+      URL: {finalUrl},
+      devtoolsLogs: {[UsesHTTP2Audit.DEFAULT_PASS]: devtoolsLog},
+    });
+  }
+
   it('fails when some resources were requested via http/1.x', () => {
-    return UsesHTTP2Audit.audit({
-      URL: {finalUrl: URL},
-      devtoolsLogs: {[UsesHTTP2Audit.DEFAULT_PASS]: []},
-      requestNetworkRecords: () => Promise.resolve(networkRecords),
-    }).then(auditResult => {
+    return UsesHTTP2Audit.audit(
+      getArtifacts(networkRecords, URL)
+    ).then(auditResult => {
       assert.equal(auditResult.rawValue, false);
       assert.ok(auditResult.displayValue.match('3 requests not'));
       assert.equal(auditResult.details.items.length, 3);
@@ -32,11 +42,9 @@ describe('Resources are fetched over http/2', () => {
 
   it('displayValue is correct when only one resource fails', () => {
     const entryWithHTTP1 = networkRecords.slice(1, 2);
-    return UsesHTTP2Audit.audit({
-      URL: {finalUrl: URL},
-      devtoolsLogs: {[UsesHTTP2Audit.DEFAULT_PASS]: []},
-      requestNetworkRecords: () => Promise.resolve(entryWithHTTP1),
-    }).then(auditResult => {
+    return UsesHTTP2Audit.audit(
+      getArtifacts(entryWithHTTP1, URL)
+    ).then(auditResult => {
       assert.ok(auditResult.displayValue.match('1 request not'));
     });
   });
@@ -47,11 +55,9 @@ describe('Resources are fetched over http/2', () => {
       record.protocol = 'h2';
     });
 
-    return UsesHTTP2Audit.audit({
-      URL: {finalUrl: URL},
-      devtoolsLogs: {[UsesHTTP2Audit.DEFAULT_PASS]: []},
-      requestNetworkRecords: () => Promise.resolve(h2Records),
-    }).then(auditResult => {
+    return UsesHTTP2Audit.audit(
+      getArtifacts(h2Records, URL)
+    ).then(auditResult => {
       assert.equal(auditResult.rawValue, true);
       assert.ok(auditResult.displayValue === '');
     });
diff --git a/lighthouse-core/test/audits/font-display-test.js b/lighthouse-core/test/audits/font-display-test.js
index 1a836ba4c..fb4d9c62e 100644
--- a/lighthouse-core/test/audits/font-display-test.js
+++ b/lighthouse-core/test/audits/font-display-test.js
@@ -8,6 +8,8 @@
 const NetworkRequest = require('../../lib/network-request');
 const Audit = require('../../audits/font-display.js');
 const assert = require('assert');
+const Runner = require('../../runner.js');
+const networkRecordsToDevtoolsLog = require('../network-records-to-devtools-log.js');
 
 /* eslint-env jest */
 const openSansFont = {
@@ -34,11 +36,10 @@ const openSansFontBold = {
 
 describe('Performance: Font Display audit', () => {
   function getArtifacts(networkRecords, fonts) {
-    return {
-      devtoolsLogs: {[Audit.DEFAULT_PASS]: []},
-      requestNetworkRecords: () => Promise.resolve(networkRecords),
+    return Object.assign({
+      devtoolsLogs: {[Audit.DEFAULT_PASS]: networkRecordsToDevtoolsLog(networkRecords)},
       Fonts: fonts,
-    };
+    }, Runner.instantiateComputedArtifacts());
   }
 
   it('fails when not all fonts have a correct font-display rule', () => {
diff --git a/lighthouse-core/test/audits/is-on-https-test.js b/lighthouse-core/test/audits/is-on-https-test.js
index c817fbec4..f158f30fb 100644
--- a/lighthouse-core/test/audits/is-on-https-test.js
+++ b/lighthouse-core/test/audits/is-on-https-test.js
@@ -5,17 +5,19 @@
  */
 'use strict';
 
+const Runner = require('../../runner.js');
 const Audit = require('../../audits/is-on-https.js');
 const assert = require('assert');
+const networkRecordsToDevtoolsLog = require('../network-records-to-devtools-log.js');
 
 /* eslint-env jest */
 
 describe('Security: HTTPS audit', () => {
   function getArtifacts(networkRecords) {
-    return {
-      devtoolsLogs: {[Audit.DEFAULT_PASS]: []},
-      requestNetworkRecords: () => Promise.resolve(networkRecords),
-    };
+    const devtoolsLog = networkRecordsToDevtoolsLog(networkRecords);
+    return Object.assign(Runner.instantiateComputedArtifacts(), {
+      devtoolsLogs: {[Audit.DEFAULT_PASS]: devtoolsLog},
+    });
   }
 
   it('fails when there is more than one insecure record', () => {
diff --git a/lighthouse-core/test/audits/load-fast-enough-for-pwa-test.js b/lighthouse-core/test/audits/load-fast-enough-for-pwa-test.js
index 283f9bccf..9f0f3abd2 100644
--- a/lighthouse-core/test/audits/load-fast-enough-for-pwa-test.js
+++ b/lighthouse-core/test/audits/load-fast-enough-for-pwa-test.js
@@ -8,7 +8,7 @@
 const FastPWAAudit = require('../../audits/load-fast-enough-for-pwa');
 const Runner = require('../../runner.js');
 const Audit = require('../../audits/audit.js');
-const mobile3GThrottling = require('../../config/constants').throttling.mobile3G;
+const mobileSlow4GThrottling = require('../../config/constants').throttling.mobileSlow4G;
 const assert = require('assert');
 
 const trace = require('../fixtures/traces/progressive-app-m60.json');
@@ -31,7 +31,7 @@ function generateArtifacts(ttiValue) {
 /* eslint-env jest */
 describe('PWA: load-fast-enough-for-pwa audit', () => {
   it('returns boolean based on TTI value', () => {
-    const settings = {throttlingMethod: 'devtools', throttling: mobile3GThrottling};
+    const settings = {throttlingMethod: 'devtools', throttling: mobileSlow4GThrottling};
     return FastPWAAudit.audit(generateArtifacts(5000), {settings}).then(result => {
       assert.equal(result.score, true, 'fixture trace is not passing audit');
       assert.equal(result.rawValue, 5000);
@@ -39,7 +39,7 @@ describe('PWA: load-fast-enough-for-pwa audit', () => {
   });
 
   it('fails a bad TTI value', () => {
-    const settings = {throttlingMethod: 'devtools', throttling: mobile3GThrottling};
+    const settings = {throttlingMethod: 'devtools', throttling: mobileSlow4GThrottling};
     return FastPWAAudit.audit(generateArtifacts(15000), {settings}).then(result => {
       assert.equal(result.score, false, 'not failing a long TTI value');
       assert.equal(result.rawValue, 15000);
@@ -54,7 +54,7 @@ describe('PWA: load-fast-enough-for-pwa audit', () => {
       devtoolsLogs: {defaultPass: devtoolsLog},
     }, Runner.instantiateComputedArtifacts());
 
-    const settings = {throttlingMethod: 'devtools', throttling: mobile3GThrottling};
+    const settings = {throttlingMethod: 'devtools', throttling: mobileSlow4GThrottling};
     const result = await FastPWAAudit.audit(artifacts, {settings});
     assert.equal(Math.round(result.rawValue), 1582);
   });
diff --git a/lighthouse-core/test/audits/mixed-content-test.js b/lighthouse-core/test/audits/mixed-content-test.js
index ed0cbf432..c36023597 100644
--- a/lighthouse-core/test/audits/mixed-content-test.js
+++ b/lighthouse-core/test/audits/mixed-content-test.js
@@ -7,22 +7,23 @@
 
 const Audit = require('../../audits/mixed-content.js');
 const assert = require('assert');
+const Runner = require('../../runner');
+const networkRecordsToDevtoolsLog = require('../network-records-to-devtools-log.js');
 
 /* eslint-env jest */
 
 describe('Mixed Content audit', () => {
   function getArtifacts(baseUrl, defaultPassRecords, mixedContentPassRecords) {
-    return {
+    const defaultDevtoolsLog = networkRecordsToDevtoolsLog(defaultPassRecords);
+    const mixedContentDevtoolsLog = networkRecordsToDevtoolsLog(mixedContentPassRecords);
+
+    return Object.assign(Runner.instantiateComputedArtifacts(), {
       MixedContent: {url: baseUrl},
-      devtoolsLogs: {[Audit.DEFAULT_PASS]: true, ['mixedContentPass']: false},
-      requestNetworkRecords: (pass) => {
-        if (pass) {
-          return Promise.resolve(defaultPassRecords);
-        } else {
-          return Promise.resolve(mixedContentPassRecords);
-        }
+      devtoolsLogs: {
+        [Audit.DEFAULT_PASS]: defaultDevtoolsLog,
+        ['mixedContentPass']: mixedContentDevtoolsLog,
       },
-    };
+    });
   }
 
   it('passes when there are no insecure resources by default', () => {
@@ -55,7 +56,7 @@ describe('Mixed Content audit', () => {
       {url: 'https://example.org/', isSecure: true, finished: true, documentURL: 'http://example.org/'},
       {url: 'https://example.org/resource1.js', isSecure: true, finished: true, documentURL: 'https://example.org'},
       {url: 'https://third-party.example.com/resource2.js', isSecure: true, finished: true, documentURL: 'https://example.org'},
-      {url: 'https://fourth-party.example.com/resource3.js', isSecure: false, finished: true, documentURL: 'https://third-party.example.com'},
+      {url: 'https://fourth-party.example.com/resource3.js', isSecure: true, finished: true, documentURL: 'https://third-party.example.com'},
     ];
     return Audit.audit(
       getArtifacts('http://example.org', defaultRecords, upgradeRecords)
diff --git a/lighthouse-core/test/audits/network-requests-test.js b/lighthouse-core/test/audits/network-requests-test.js
index f9b5e2692..12b442ad8 100644
--- a/lighthouse-core/test/audits/network-requests-test.js
+++ b/lighthouse-core/test/audits/network-requests-test.js
@@ -8,6 +8,7 @@
 const NetworkRequests = require('../../audits/network-requests');
 const Runner = require('../../runner.js');
 const assert = require('assert');
+const networkRecordsToDevtoolsLog = require('../network-records-to-devtools-log.js');
 
 const acceptableDevToolsLog = require('../fixtures/traces/progressive-app-m60.devtools.log.json');
 
@@ -38,7 +39,11 @@ describe('Network requests audit', () => {
       {url: 'https://example.com/1', startTime: 15.5, endTime: -1},
     ];
 
-    const artifacts = {devtoolsLogs: {}, requestNetworkRecords: () => Promise.resolve(records)};
+    const artifacts = Object.assign(Runner.instantiateComputedArtifacts(), {
+      devtoolsLogs: {
+        [NetworkRequests.DEFAULT_PASS]: networkRecordsToDevtoolsLog(records),
+      },
+    });
     const output = await NetworkRequests.audit(artifacts);
     assert.equal(output.details.items[0].startTime, 0);
     assert.equal(output.details.items[0].endTime, 500);
diff --git a/lighthouse-core/test/audits/redirects-test.js b/lighthouse-core/test/audits/redirects-test.js
index c1624ec68..ecfb76adb 100644
--- a/lighthouse-core/test/audits/redirects-test.js
+++ b/lighthouse-core/test/audits/redirects-test.js
@@ -5,7 +5,8 @@
  */
 'use strict';
 
-const Audit = require('../../audits/redirects.js');
+const Runner = require('../../runner.js');
+const RedirectsAudit = require('../../audits/redirects.js');
 const assert = require('assert');
 
 /* eslint-env jest */
@@ -62,16 +63,15 @@ describe('Performance: Redirects audit', () => {
   let nodeTimings;
 
   const mockArtifacts = (mockChain) => {
-    return {
+    return Object.assign(Runner.instantiateComputedArtifacts(), {
       traces: {},
-      devtoolsLogs: {},
+      devtoolsLogs: {defaultPass: []},
       requestLanternInteractive: () => ({pessimisticEstimate: {nodeTimings}}),
       requestTraceOfTab: () => {},
-      requestNetworkRecords: () => [],
       requestMainResource: function() {
         return Promise.resolve(mockChain);
       },
-    };
+    });
   };
 
   it('fails when 3 redirects detected', () => {
@@ -82,7 +82,7 @@ describe('Performance: Redirects audit', () => {
       [{type: 'network', record: {url: 'https://m.example.com/final'}}, {startTime: 17000}],
     ]);
 
-    return Audit.audit(mockArtifacts(FAILING_THREE_REDIRECTS), {}).then(output => {
+    return RedirectsAudit.audit(mockArtifacts(FAILING_THREE_REDIRECTS), {}).then(output => {
       assert.equal(output.score, 0);
       assert.equal(output.details.items.length, 4);
       assert.equal(output.rawValue, 17000);
@@ -96,7 +96,7 @@ describe('Performance: Redirects audit', () => {
       [{type: 'network', record: {url: 'https://www.lisairish.com/'}}, {startTime: 638}],
     ]);
 
-    return Audit.audit(mockArtifacts(FAILING_TWO_REDIRECTS), {}).then(output => {
+    return RedirectsAudit.audit(mockArtifacts(FAILING_TWO_REDIRECTS), {}).then(output => {
       assert.equal(Math.round(output.score * 100) / 100, 0.56);
       assert.equal(output.details.items.length, 3);
       assert.equal(Math.round(output.rawValue), 638);
@@ -109,7 +109,7 @@ describe('Performance: Redirects audit', () => {
       [{type: 'network', record: {url: 'https://www.lisairish.com/'}}, {startTime: 510}],
     ]);
 
-    return Audit.audit(mockArtifacts(SUCCESS_ONE_REDIRECT), {}).then(output => {
+    return RedirectsAudit.audit(mockArtifacts(SUCCESS_ONE_REDIRECT), {}).then(output => {
       // If === 1 redirect, perfect score is expected, regardless of latency
       assert.equal(output.score, 1);
       // We will still generate a table and show wasted time
@@ -119,7 +119,7 @@ describe('Performance: Redirects audit', () => {
   });
 
   it('passes when no redirect detected', () => {
-    return Audit.audit(mockArtifacts(SUCCESS_NOREDIRECT), {}).then(output => {
+    return RedirectsAudit.audit(mockArtifacts(SUCCESS_NOREDIRECT), {}).then(output => {
       assert.equal(output.score, 1);
       assert.equal(output.details.items.length, 0);
       assert.equal(output.rawValue, 0);
diff --git a/lighthouse-core/test/audits/seo/http-status-code-test.js b/lighthouse-core/test/audits/seo/http-status-code-test.js
index 38ba93c69..8dbde2d3f 100644
--- a/lighthouse-core/test/audits/seo/http-status-code-test.js
+++ b/lighthouse-core/test/audits/seo/http-status-code-test.js
@@ -7,6 +7,8 @@
 
 const HTTPStatusCodeAudit = require('../../../audits/seo/http-status-code.js');
 const assert = require('assert');
+const Runner = require('../../../runner.js');
+const networkRecordsToDevtoolsLog = require('../../network-records-to-devtools-log.js');
 
 /* eslint-env jest */
 
@@ -15,14 +17,17 @@ describe('SEO: HTTP code audit', () => {
     const statusCodes = [403, 404, 500];
 
     const allRuns = statusCodes.map(statusCode => {
+      const finalUrl = 'https://example.com';
       const mainResource = {
+        url: finalUrl,
         statusCode,
       };
-      const artifacts = {
-        devtoolsLogs: {[HTTPStatusCodeAudit.DEFAULT_PASS]: []},
-        requestNetworkRecords: () => Promise.resolve(),
-        requestMainResource: () => Promise.resolve(mainResource),
-      };
+      const devtoolsLog = networkRecordsToDevtoolsLog([mainResource]);
+
+      const artifacts = Object.assign(Runner.instantiateComputedArtifacts(), {
+        devtoolsLogs: {[HTTPStatusCodeAudit.DEFAULT_PASS]: devtoolsLog},
+        URL: {finalUrl},
+      });
 
       return HTTPStatusCodeAudit.audit(artifacts).then(auditResult => {
         assert.equal(auditResult.rawValue, false);
@@ -34,15 +39,17 @@ describe('SEO: HTTP code audit', () => {
   });
 
   it('passes when status code is successful', () => {
+    const finalUrl = 'https://example.com';
     const mainResource = {
+      url: finalUrl,
       statusCode: 200,
     };
+    const devtoolsLog = networkRecordsToDevtoolsLog([mainResource]);
 
-    const artifacts = {
-      devtoolsLogs: {[HTTPStatusCodeAudit.DEFAULT_PASS]: []},
-      requestNetworkRecords: () => Promise.resolve(),
-      requestMainResource: () => Promise.resolve(mainResource),
-    };
+    const artifacts = Object.assign(Runner.instantiateComputedArtifacts(), {
+      devtoolsLogs: {[HTTPStatusCodeAudit.DEFAULT_PASS]: devtoolsLog},
+      URL: {finalUrl},
+    });
 
     return HTTPStatusCodeAudit.audit(artifacts).then(auditResult => {
       assert.equal(auditResult.rawValue, true);
diff --git a/lighthouse-core/test/audits/uses-rel-preconnect-test.js b/lighthouse-core/test/audits/uses-rel-preconnect-test.js
index f8a414ef4..0efec767f 100644
--- a/lighthouse-core/test/audits/uses-rel-preconnect-test.js
+++ b/lighthouse-core/test/audits/uses-rel-preconnect-test.js
@@ -10,6 +10,8 @@
 
 const UsesRelPreconnect = require('../../audits/uses-rel-preconnect.js');
 const assert = require('assert');
+const Runner = require('../../runner.js');
+const networkRecordsToDevtoolsLog = require('../network-records-to-devtools-log.js');
 
 const mainResource = {
   url: 'https://www.example.com/',
@@ -36,17 +38,13 @@ describe('Performance: uses-rel-preconnect audit', () => {
       mainResource,
       {
         url: 'https://www.example.com/request',
-        parsedURL: {
-          securityOrigin: 'https://www.example.com',
-        },
       },
     ];
-    const artifacts = {
-      devtoolsLogs: {[UsesRelPreconnect.DEFAULT_PASS]: []},
-      requestNetworkRecords: () => Promise.resolve(networkRecords),
+    const artifacts = Object.assign(Runner.instantiateComputedArtifacts(), {
+      devtoolsLogs: {[UsesRelPreconnect.DEFAULT_PASS]: networkRecordsToDevtoolsLog(networkRecords)},
       requestMainResource: () => Promise.resolve(mainResource),
       requestLoadSimulator: () => Promise.resolve(simulator),
-    };
+    });
 
     const {score, rawValue, details} = await UsesRelPreconnect.audit(artifacts, {});
     assert.equal(score, 1);
@@ -62,12 +60,11 @@ describe('Performance: uses-rel-preconnect audit', () => {
         initiator: mainResource,
       },
     ];
-    const artifacts = {
-      devtoolsLogs: {[UsesRelPreconnect.DEFAULT_PASS]: []},
-      requestNetworkRecords: () => Promise.resolve(networkRecords),
+    const artifacts = Object.assign(Runner.instantiateComputedArtifacts(), {
+      devtoolsLogs: {[UsesRelPreconnect.DEFAULT_PASS]: networkRecordsToDevtoolsLog(networkRecords)},
       requestMainResource: () => Promise.resolve(mainResource),
       requestLoadSimulator: () => Promise.resolve(simulator),
-    };
+    });
 
     const {score, rawValue, details} = await UsesRelPreconnect.audit(artifacts, {});
     assert.equal(score, 1);
@@ -83,12 +80,11 @@ describe('Performance: uses-rel-preconnect audit', () => {
         initiator: {},
       },
     ];
-    const artifacts = {
-      devtoolsLogs: {[UsesRelPreconnect.DEFAULT_PASS]: []},
-      requestNetworkRecords: () => Promise.resolve(networkRecords),
+    const artifacts = Object.assign(Runner.instantiateComputedArtifacts(), {
+      devtoolsLogs: {[UsesRelPreconnect.DEFAULT_PASS]: networkRecordsToDevtoolsLog(networkRecords)},
       requestMainResource: () => Promise.resolve(mainResource),
       requestLoadSimulator: () => Promise.resolve(simulator),
-    };
+    });
 
     const {score, rawValue, details} = await UsesRelPreconnect.audit(artifacts, {});
     assert.equal(score, 1);
@@ -110,12 +106,11 @@ describe('Performance: uses-rel-preconnect audit', () => {
         },
       },
     ];
-    const artifacts = {
-      devtoolsLogs: {[UsesRelPreconnect.DEFAULT_PASS]: []},
-      requestNetworkRecords: () => Promise.resolve(networkRecords),
+    const artifacts = Object.assign(Runner.instantiateComputedArtifacts(), {
+      devtoolsLogs: {[UsesRelPreconnect.DEFAULT_PASS]: networkRecordsToDevtoolsLog(networkRecords)},
       requestMainResource: () => Promise.resolve(mainResource),
       requestLoadSimulator: () => Promise.resolve(simulator),
-    };
+    });
 
     const {score, rawValue, details} = await UsesRelPreconnect.audit(artifacts, {});
     assert.equal(score, 1);
@@ -129,15 +124,14 @@ describe('Performance: uses-rel-preconnect audit', () => {
       {
         url: 'https://cdn.example.com/request',
         initiator: {},
-        _startTime: 16,
+        startTime: 16,
       },
     ];
-    const artifacts = {
-      devtoolsLogs: {[UsesRelPreconnect.DEFAULT_PASS]: []},
-      requestNetworkRecords: () => Promise.resolve(networkRecords),
+    const artifacts = Object.assign(Runner.instantiateComputedArtifacts(), {
+      devtoolsLogs: {[UsesRelPreconnect.DEFAULT_PASS]: networkRecordsToDevtoolsLog(networkRecords)},
       requestMainResource: () => Promise.resolve(mainResource),
       requestLoadSimulator: () => Promise.resolve(simulator),
-    };
+    });
 
     const {score, rawValue, details} = await UsesRelPreconnect.audit(artifacts, {});
     assert.equal(score, 1);
@@ -151,10 +145,6 @@ describe('Performance: uses-rel-preconnect audit', () => {
       {
         url: 'https://cdn.example.com/first',
         initiator: {},
-        parsedURL: {
-          scheme: 'https',
-          securityOrigin: 'https://cdn.example.com',
-        },
         startTime: 2,
         timing: {
           dnsStart: 100,
@@ -165,10 +155,6 @@ describe('Performance: uses-rel-preconnect audit', () => {
       {
         url: 'https://cdn.example.com/second',
         initiator: {},
-        parsedURL: {
-          scheme: 'https',
-          securityOrigin: 'https://cdn.example.com',
-        },
         startTime: 3,
         timing: {
           dnsStart: 300,
@@ -177,12 +163,11 @@ describe('Performance: uses-rel-preconnect audit', () => {
         },
       },
     ];
-    const artifacts = {
-      devtoolsLogs: {[UsesRelPreconnect.DEFAULT_PASS]: []},
-      requestNetworkRecords: () => Promise.resolve(networkRecords),
+    const artifacts = Object.assign(Runner.instantiateComputedArtifacts(), {
+      devtoolsLogs: {[UsesRelPreconnect.DEFAULT_PASS]: networkRecordsToDevtoolsLog(networkRecords)},
       requestMainResource: () => Promise.resolve(mainResource),
       requestLoadSimulator: () => Promise.resolve(simulator),
-    };
+    });
 
     const {rawValue, extendedInfo} = await UsesRelPreconnect.audit(artifacts, {});
     assert.equal(rawValue, 200);
@@ -196,12 +181,8 @@ describe('Performance: uses-rel-preconnect audit', () => {
     const networkRecords = [
       mainResource,
       {
-        url: 'https://cdn.example.com/first',
+        url: 'http://cdn.example.com/first',
         initiator: {},
-        parsedURL: {
-          scheme: 'http',
-          securityOrigin: 'http://cdn.example.com',
-        },
         startTime: 2,
         timing: {
           dnsStart: 100,
@@ -212,10 +193,6 @@ describe('Performance: uses-rel-preconnect audit', () => {
       {
         url: 'https://othercdn.example.com/second',
         initiator: {},
-        parsedURL: {
-          scheme: 'https',
-          securityOrigin: 'https://othercdn.example.com',
-        },
         startTime: 1.2,
         timing: {
           dnsStart: 100,
@@ -224,12 +201,11 @@ describe('Performance: uses-rel-preconnect audit', () => {
         },
       },
     ];
-    const artifacts = {
-      devtoolsLogs: {[UsesRelPreconnect.DEFAULT_PASS]: []},
-      requestNetworkRecords: () => Promise.resolve(networkRecords),
+    const artifacts = Object.assign(Runner.instantiateComputedArtifacts(), {
+      devtoolsLogs: {[UsesRelPreconnect.DEFAULT_PASS]: networkRecordsToDevtoolsLog(networkRecords)},
       requestMainResource: () => Promise.resolve(mainResource),
       requestLoadSimulator: () => Promise.resolve(simulator),
-    };
+    });
 
     simulatorOptions = {
       rtt: 100,
diff --git a/lighthouse-core/test/audits/uses-rel-preload-test.js b/lighthouse-core/test/audits/uses-rel-preload-test.js
index 9b0ce4889..aee4cb13c 100644
--- a/lighthouse-core/test/audits/uses-rel-preload-test.js
+++ b/lighthouse-core/test/audits/uses-rel-preload-test.js
@@ -15,6 +15,7 @@ const assert = require('assert');
 const Runner = require('../../runner');
 const pwaTrace = require('../fixtures/traces/progressive-app-m60.json');
 const pwaDevtoolsLog = require('../fixtures/traces/progressive-app-m60.devtools.log.json');
+const networkRecordsToDevtoolsLog = require('../network-records-to-devtools-log.js');
 
 const defaultMainResource = {
   _endTime: 1,
@@ -25,16 +26,15 @@ describe('Performance: uses-rel-preload audit', () => {
   let mockSimulator;
 
   const mockArtifacts = (networkRecords, mainResource = defaultMainResource) => {
-    return {
+    return Object.assign(Runner.instantiateComputedArtifacts(), {
       traces: {[UsesRelPreload.DEFAULT_PASS]: {traceEvents: []}},
-      devtoolsLogs: {[UsesRelPreload.DEFAULT_PASS]: []},
+      devtoolsLogs: {[UsesRelPreload.DEFAULT_PASS]: networkRecordsToDevtoolsLog(networkRecords)},
       requestLoadSimulator: () => mockSimulator,
       requestPageDependencyGraph: () => mockGraph,
-      requestNetworkRecords: () => networkRecords,
       requestMainResource: () => {
         return Promise.resolve(mainResource);
       },
-    };
+    });
   };
 
   afterEach(() => {
@@ -50,11 +50,9 @@ describe('Performance: uses-rel-preload audit', () => {
     const networkRecords = [
       {
         requestId: '2',
-        resourceType: 'Document',
         priority: 'High',
         isLinkPreload: false,
         url: 'http://example.com:3000',
-        redirects: [''],
       },
       {
         requestId: '2:redirect',
@@ -62,7 +60,6 @@ describe('Performance: uses-rel-preload audit', () => {
         priority: 'High',
         isLinkPreload: false,
         url: 'http://www.example.com:3000',
-        redirects: [''],
       },
       {
         requestId: '3',
@@ -160,7 +157,7 @@ describe('Performance: uses-rel-preload audit', () => {
     const networkRecords = [
       {
         requestId: '3',
-        _startTime: 10,
+        startTime: 10,
         isLinkPreload: true,
         url: 'http://www.example.com/script.js',
       },
@@ -177,7 +174,7 @@ describe('Performance: uses-rel-preload audit', () => {
       {
         requestId: '3',
         protocol: 'data',
-        _startTime: 10,
+        startTime: 10,
       },
     ];
 
@@ -192,7 +189,7 @@ describe('Performance: uses-rel-preload audit', () => {
       {
         requestId: '3',
         protocol: 'blob',
-        _startTime: 10,
+        startTime: 10,
       },
     ];
 
diff --git a/lighthouse-core/test/gather/computed/main-resource-test.js b/lighthouse-core/test/gather/computed/main-resource-test.js
index cfd15d4c4..4ee2af556 100644
--- a/lighthouse-core/test/gather/computed/main-resource-test.js
+++ b/lighthouse-core/test/gather/computed/main-resource-test.js
@@ -9,6 +9,7 @@
 
 const Runner = require('../../../runner.js');
 const assert = require('assert');
+const networkRecordsToDevtoolsLog = require('../../network-records-to-devtools-log.js');
 
 describe('MainResource computed artifact', () => {
   let computedArtifacts;
@@ -25,11 +26,11 @@ describe('MainResource computed artifact', () => {
       {url: 'http://example.com'},
       record,
     ];
-    computedArtifacts.requestNetworkRecords = _ => Promise.resolve(networkRecords);
     const URL = {finalUrl: 'https://example.com'};
+    const devtoolsLog = networkRecordsToDevtoolsLog(networkRecords);
 
-    return computedArtifacts.requestMainResource({URL}).then(output => {
-      assert.equal(output, record);
+    return computedArtifacts.requestMainResource({URL, devtoolsLog}).then(output => {
+      assert.equal(output.url, record.url);
     });
   });
 
@@ -37,10 +38,10 @@ describe('MainResource computed artifact', () => {
     const networkRecords = [
       {url: 'https://example.com'},
     ];
-    computedArtifacts.requestNetworkRecords = _ => Promise.resolve(networkRecords);
     const URL = {finalUrl: 'https://m.example.com'};
+    const devtoolsLog = networkRecordsToDevtoolsLog(networkRecords);
 
-    return computedArtifacts.requestMainResource({URL}).then(() => {
+    return computedArtifacts.requestMainResource({URL, devtoolsLog}).then(() => {
       assert.ok(false, 'should have thrown');
     }).catch(err => {
       assert.equal(err.message, 'Unable to identify the main resource');
@@ -63,9 +64,9 @@ describe('MainResource computed artifact', () => {
       {url: 'https://beta.httparchive.org/reports/state-of-the-web'},
     ];
 
-    computedArtifacts.requestNetworkRecords = _ => Promise.resolve(networkRecords);
     const URL = {finalUrl: 'https://beta.httparchive.org/reports/state-of-the-web#pctHttps'};
-    const artifacts = {URL};
+    const devtoolsLog = networkRecordsToDevtoolsLog(networkRecords);
+    const artifacts = {URL, devtoolsLog};
 
     return computedArtifacts.requestMainResource(artifacts).then(output => {
       assert.equal(output.url, 'https://beta.httparchive.org/reports/state-of-the-web');
diff --git a/lighthouse-core/test/network-records-to-devtools-log.js b/lighthouse-core/test/network-records-to-devtools-log.js
new file mode 100644
index 000000000..4847e99d3
--- /dev/null
+++ b/lighthouse-core/test/network-records-to-devtools-log.js
@@ -0,0 +1,210 @@
+/**
+ * @license Copyright 2018 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 NetworkRecorder = require('../../lighthouse-core/lib/network-recorder.js');
+/** @typedef {import('../../lighthouse-core/lib/network-request.js')} NetworkRequest */
+
+const idBase = '127122';
+const exampleUrl = 'https://testingurl.com/';
+const redirectSuffix = ':redirect';
+
+/* eslint-env jest */
+
+/**
+ * Extract requestId without any `:redirect` strings.
+ * @param {Partial<NetworkRequest>} requestId
+ */
+function getBaseRequestId(record) {
+  if (!record.requestId) return;
+
+  const match = /^([\w.]+)(?::redirect)*$/.exec(record.requestId);
+  return match && match[1];
+}
+
+/**
+ * @param {Array<HeaderEntry>=} headersArray
+ * @return {LH.Crdp.Network.Headers}
+ */
+function headersArrayToHeadersDict(headersArray = []) {
+  const headersDict = {};
+  headersArray.forEach(headerItem => {
+    const value = headersDict[headerItem.name] ? headersDict[headerItem.name] + '\n' : '';
+    headersDict[headerItem.name] = value + headerItem.value;
+  });
+
+  return headersDict;
+}
+
+/**
+ * @param {Partial<NetworkRequest>} networkRecord
+ * @return {LH.Protocol.RawEventMessage}
+ */
+function getRequestWillBeSentEvent(networkRecord, index) {
+  const initiator = networkRecord.isLinkPreload ? {type: 'preload'} :
+      networkRecord.initiator || {type: 'other'};
+
+  return {
+    method: 'Network.requestWillBeSent',
+    params: {
+      requestId: getBaseRequestId(networkRecord) || `${idBase}.${index}`,
+      documentURL: networkRecord.documentURL || exampleUrl,
+      request: {
+        url: networkRecord.url || exampleUrl,
+        method: networkRecord.requestMethod || 'GET',
+        headers: {},
+        initialPriority: networkRecord.priority || 'Low',
+      },
+      timestamp: networkRecord.startTime || 0,
+      wallTime: 0,
+      initiator: initiator,
+      type: networkRecord.resourceType || 'Document',
+      frameId: `${idBase}.1`,
+      redirectResponse: networkRecord.redirectResponse,
+    },
+  };
+}
+
+/**
+ * @param {Partial<NetworkRequest>} networkRecord
+ * @return {LH.Protocol.RawEventMessage}
+ */
+function getResponseReceivedEvent(networkRecord, index) {
+  const headers = headersArrayToHeadersDict(networkRecord.responseHeaders);
+  const timing = networkRecord.timing;
+  if (timing && timing.requestTime === undefined) {
+    timing.requestTime = networkRecord.startTime || 0;
+  }
+
+  return {
+    method: 'Network.responseReceived',
+    params: {
+      requestId: getBaseRequestId(networkRecord) || `${idBase}.${index}`,
+      timestamp: networkRecord.responseReceivedTime || 1,
+      type: networkRecord.resourceType || undefined,
+      response: {
+        url: networkRecord.url || exampleUrl,
+        status: networkRecord.statusCode || 200,
+        headers,
+        mimeType: networkRecord.mimeType || 'text/html',
+        connectionReused: networkRecord.connectionReused || false,
+        connectionId: networkRecord.connectionId || 140,
+        fromDiskCache: networkRecord.fromDiskCache || undefined,
+        fromServiceWorker: networkRecord.fromServiceWorker || undefined,
+        encodedDataLength: networkRecord.transferSize || 0,
+        timing,
+        protocol: networkRecord.protocol || 'http/1.1',
+      },
+      frameId: `${idBase}.1`,
+    },
+  };
+}
+
+/**
+ * @param {Partial<NetworkRequest>} networkRecord
+ * @return {LH.Protocol.RawEventMessage}
+ */
+function getDataReceivedEvent(networkRecord, index) {
+  return {
+    method: 'Network.dataReceived',
+    params: {
+      requestId: getBaseRequestId(networkRecord) || `${idBase}.${index}`,
+      dataLength: networkRecord.resourceSize || 0,
+      encodedDataLength: networkRecord.transferSize || 0,
+    },
+  };
+}
+
+/**
+ * @param {Partial<NetworkRequest>} networkRecord
+ * @return {LH.Protocol.RawEventMessage}
+ */
+function getLoadingFinishedEvent(networkRecord, index) {
+  return {
+    method: 'Network.loadingFinished',
+    params: {
+      requestId: getBaseRequestId(networkRecord) || `${idBase}.${index}`,
+      timestamp: networkRecord.endTime || 3,
+      encodedDataLength: networkRecord.transferSize || 0,
+    },
+  };
+}
+
+/**
+ * Returns true if `record` is redirected by another record.
+ * @param {Array<Partial<NetworkRequest>>} networkRecords
+ * @param {Partial<NetworkRequest>} record
+ * @return {boolean}
+ */
+function willBeRedirected(networkRecords, record) {
+  if (!record.requestId) {
+    return false;
+  }
+
+  const redirectId = record.requestId + redirectSuffix;
+  return networkRecords.some(otherRecord => otherRecord.requestId === redirectId);
+}
+
+/**
+ * If `record` is a redirect of another record, create a fake redirect respose
+ * to keep the original request defined correctly.
+ * @param {Array<Partial<NetworkRequest>>} networkRecords
+ * @param {Partial<NetworkRequest>} record
+ * @return {Partial<NetworkRequest>}
+ */
+function addRedirectResponseIfNeeded(networkRecords, record) {
+  if (!record.requestId || !record.requestId.endsWith(redirectSuffix)) {
+    return record;
+  }
+
+  const originalId = record.requestId.slice(0, -redirectSuffix.length);
+  const originalRecord = networkRecords.find(record => record.requestId === originalId);
+  if (!originalRecord) {
+    throw new Error(`redirect with id ${record.requestId} has no original request`);
+  }
+
+  // populate `redirectResponse` with original's data, more or less.
+  const originalResponse = getResponseReceivedEvent(originalRecord).params.response;
+  originalResponse.status = originalRecord.statusCode || 302;
+  return Object.assign({}, record, {redirectResponse: originalResponse});
+}
+
+/**
+ * Generate a devtoolsLog that can regenerate the passed-in `networkRecords`.
+ * Generally best at replicating artificial or pruned networkRecords used for
+ * testing. If run from a test runner, verifies that everything in
+ * `networkRecords` will be in any network records generated from the output
+ * (use `skipVerification` to manually skip this assertion).
+ * @param {Array<Partial<NetworkRequest>>} networkRecords
+ * @param {{skipVerification?: boolean}=} options
+ * @return {LH.DevtoolsLog}
+ */
+function networkRecordsToDevtoolsLog(networkRecords, options = {}) {
+  const devtoolsLog = [];
+  networkRecords.forEach((networkRecord, index) => {
+    networkRecord = addRedirectResponseIfNeeded(networkRecords, networkRecord);
+    devtoolsLog.push(getRequestWillBeSentEvent(networkRecord, index));
+
+    if (willBeRedirected(networkRecords, networkRecord)) {
+      // If record is going to redirect, only issue the first event.
+      return;
+    }
+
+    devtoolsLog.push(getResponseReceivedEvent(networkRecord, index));
+    devtoolsLog.push(getDataReceivedEvent(networkRecord, index));
+    devtoolsLog.push(getLoadingFinishedEvent(networkRecord, index));
+  });
+
+  // If in a test, assert that the log will turn into an equivalent networkRecords.
+  if (global.expect && !options.skipVerification) {
+    const roundTrippedNetworkRecords = NetworkRecorder.recordsFromLogs(devtoolsLog);
+    expect(roundTrippedNetworkRecords).toMatchObject(networkRecords);
+  }
+
+  return devtoolsLog;
+}
+
+module.exports = networkRecordsToDevtoolsLog;
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
new file mode 100644
index 000000000..a5733a65f
--- /dev/null
+++ b/lighthouse-core/test/report/html/renderer/report-ui-features-test.js
@@ -0,0 +1,107 @@
+/**
+ * @license Copyright 2017 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-env jest */
+
+const assert = require('assert');
+const fs = require('fs');
+const jsdom = require('jsdom');
+const Util = require('../../../../report/html/renderer/util.js');
+const URL = require('../../../../lib/url-shim');
+const DOM = require('../../../../report/html/renderer/dom.js');
+const DetailsRenderer = require('../../../../report/html/renderer/details-renderer.js');
+const ReportUIFeatures = require('../../../../report/html/renderer/report-ui-features.js');
+const CategoryRenderer = require('../../../../report/html/renderer/category-renderer.js');
+// lazy loaded because it depends on CategoryRenderer to be available globally
+let PerformanceCategoryRenderer = null;
+const CriticalRequestChainRenderer = require(
+    '../../../../report/html/renderer/crc-details-renderer.js');
+const ReportRenderer = require('../../../../report/html/renderer/report-renderer.js');
+const sampleResultsOrig = require('../../../results/sample_v2.json');
+
+const TEMPLATE_FILE = fs.readFileSync(__dirname +
+    '/../../../../report/html/templates.html', 'utf8');
+const TEMPLATE_FILE_REPORT = fs.readFileSync(__dirname +
+  '/../../../../report/html/report-template.html', 'utf8');
+
+describe('ReportUIFeatures', () => {
+  let renderer;
+  let reportUIFeatures;
+  let sampleResults;
+
+  beforeAll(() => {
+    global.URL = URL;
+    global.Util = Util;
+    global.ReportUIFeatures = ReportUIFeatures;
+    global.CriticalRequestChainRenderer = CriticalRequestChainRenderer;
+    global.DetailsRenderer = DetailsRenderer;
+    global.CategoryRenderer = CategoryRenderer;
+    if (!PerformanceCategoryRenderer) {
+      PerformanceCategoryRenderer =
+        require('../../../../report/html/renderer/performance-category-renderer.js');
+    }
+    global.PerformanceCategoryRenderer = PerformanceCategoryRenderer;
+
+    // Stub out matchMedia for Node.
+    global.matchMedia = function() {
+      return {
+        addListener: function() {},
+      };
+    };
+
+    const document = jsdom.jsdom(TEMPLATE_FILE);
+    const documentReport = jsdom.jsdom(TEMPLATE_FILE_REPORT);
+    global.self = document.defaultView;
+    global.self.matchMedia = function() {
+      return {
+        addListener: function() {},
+      };
+    };
+
+    global.window = {};
+    global.window.getComputedStyle = function() {
+      return {
+        marginTop: '10px',
+        height: '10px',
+      };
+    };
+
+    const dom = new DOM(document);
+    const dom2 = new DOM(documentReport);
+    const detailsRenderer = new DetailsRenderer(dom);
+    const categoryRenderer = new CategoryRenderer(dom, detailsRenderer);
+    renderer = new ReportRenderer(dom, categoryRenderer);
+    sampleResults = Util.prepareReportResult(sampleResultsOrig);
+    reportUIFeatures = new ReportUIFeatures(dom2);
+  });
+
+  afterAll(() => {
+    global.self = undefined;
+    global.URL = undefined;
+    global.Util = undefined;
+    global.ReportUIFeatures = undefined;
+    global.matchMedia = undefined;
+    global.self.matchMedia = undefined;
+    global.CriticalRequestChainRenderer = undefined;
+    global.DetailsRenderer = undefined;
+    global.CategoryRenderer = undefined;
+    global.PerformanceCategoryRenderer = undefined;
+    global.window = undefined;
+  });
+
+  describe('initFeature', () => {
+    it('should init a report', () => {
+      // render a report onto the UIFeature dom
+      const container = reportUIFeatures._dom._document.body;
+      renderer.renderReport(sampleResults, container);
+
+      assert.equal(reportUIFeatures.json, undefined);
+      reportUIFeatures.initFeatures(Util.prepareReportResult(sampleResults));
+      assert.ok(reportUIFeatures.json);
+    });
+  });
+});
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index 7451b200a..a5a0e7770 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -101,8 +101,8 @@
     },
     "load-fast-enough-for-pwa": {
       "id": "load-fast-enough-for-pwa",
-      "title": "Page load is fast enough on 3G",
-      "description": "A fast page load over a 3G network ensures a good mobile user experience. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/fast-3g).",
+      "title": "Page load is fast enough on mobile networks",
+      "description": "A fast page load over a cellular network ensures a good mobile user experience. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/fast-3g).",
       "score": 1,
       "scoreDisplayMode": "binary",
       "rawValue": 4927.278
@@ -2376,7 +2376,7 @@
             "vulnCount": 1,
             "detectedLib": {
               "text": "jQuery@2.1.1",
-              "url": "https://snyk.io/vuln/npm:jquery?lh=2.1.1",
+              "url": "https://snyk.io/vuln/npm:jquery?lh=2.1.1&utm_source=lighthouse&utm_medium=ref&utm_campaign=audit",
               "type": "link"
             }
           }
@@ -3448,7 +3448,7 @@
       "errorLabel": "Error!",
       "errorMissingAuditInfo": "Report error: no audit information",
       "labDataTitle": "Lab Data",
-      "lsPerformanceCategoryDescription": "[Lighthouse](https://developers.google.com/web/tools/lighthouse/) analysis of the current page on emulated 3G. Values are estimated and may vary.",
+      "lsPerformanceCategoryDescription": "[Lighthouse](https://developers.google.com/web/tools/lighthouse/) analysis of the current page on an emulated mobile network. Values are estimated and may vary.",
       "manualAuditsGroupTitle": "Additional items to manually check",
       "notApplicableAuditsGroupTitle": "Not applicable",
       "opportunityResourceColumnLabel": "Opportunity",
diff --git a/lighthouse-extension/test/app/src/lighthouse-ext-background-test.js b/lighthouse-extension/test/app/src/lighthouse-lr-background-test.js
similarity index 95%
rename from lighthouse-extension/test/app/src/lighthouse-ext-background-test.js
rename to lighthouse-extension/test/app/src/lighthouse-lr-background-test.js
index c66858d8e..757224c31 100644
--- a/lighthouse-extension/test/app/src/lighthouse-ext-background-test.js
+++ b/lighthouse-extension/test/app/src/lighthouse-lr-background-test.js
@@ -6,12 +6,12 @@
 'use strict';
 
 const assert = require('assert');
-const lhBackground = require('../../../app/src/lighthouse-ext-background.js');
+const lhBackground = require('../../../app/src/lighthouse-lr-background.js');
 const LHError = require('../../../../lighthouse-core/lib/lh-error.js');
 
 /* eslint-env mocha */
 
-describe('lighthouse-ext-background', () => {
+describe('lighthouse-lr-background', () => {
   describe('#runLighthouseInLR', () => {
     it('returns a runtimeError LHR when lighthouse throws a runtimeError', async () => {
       const connectionError = new LHError(LHError.errors.FAILED_DOCUMENT_REQUEST);