Repository: GoogleChrome/lighthouse
Pull request #12281 (2021-03-22 20:49:05)

misc: tweak unused audits strings (remove -> reduce)
I think "remove" suggests (at least a little bit) that _all_ unused JS/CSS should be removed. While that would be ideal, in practice that really is not possible. Slight change to "reduce" should avoid this.


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 37c6d3602..f1ae3f5b8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -127,7 +127,7 @@ jobs:
 
     # For windows, just test the potentially platform-specific code.
     - name: yarn unit-cli
-      run: yarn unit-cli || yarn unit-cli --onlyFailures
+      run: yarn unit-cli
       if: matrix.os == 'windows-latest'
     - run: yarn diff:sample-json
       if: matrix.os == 'windows-latest'
diff --git a/lighthouse-core/audits/byte-efficiency/unused-css-rules.js b/lighthouse-core/audits/byte-efficiency/unused-css-rules.js
index 39d1eb9d2..9e372bec5 100644
--- a/lighthouse-core/audits/byte-efficiency/unused-css-rules.js
+++ b/lighthouse-core/audits/byte-efficiency/unused-css-rules.js
@@ -10,11 +10,11 @@ const UnusedCSS = require('../../computed/unused-css.js');
 const i18n = require('../../lib/i18n/i18n.js');
 
 const UIStrings = {
-  /** Imperative title of a Lighthouse audit that tells the user to remove content from their CSS that isn’t needed immediately and instead load that content at a later time. This is displayed in a list of audit titles that Lighthouse generates. */
-  title: 'Remove unused CSS',
+  /** Imperative title of a Lighthouse audit that tells the user to reduce content from their CSS that isn’t needed immediately and instead load that content at a later time. This is displayed in a list of audit titles that Lighthouse generates. */
+  title: 'Reduce unused CSS',
   /** Description of a Lighthouse audit that tells the user *why* they should defer loading any content in CSS that isn’t needed at page load. This is displayed after a user expands the section to see more. No word length limits. 'Learn More' becomes link text to additional documentation. */
-  description: 'Remove dead rules from stylesheets and defer the loading of CSS not used for ' +
-    'above-the-fold content to reduce unnecessary bytes consumed by network activity. ' +
+  description: 'Reduce unused rules from stylesheets and defer CSS not used for ' +
+    'above-the-fold content to decrease bytes consumed by network activity. ' +
     '[Learn more](https://web.dev/unused-css-rules/).',
 };
 
diff --git a/lighthouse-core/audits/byte-efficiency/unused-javascript.js b/lighthouse-core/audits/byte-efficiency/unused-javascript.js
index 31dfd3aaf..3063508f9 100644
--- a/lighthouse-core/audits/byte-efficiency/unused-javascript.js
+++ b/lighthouse-core/audits/byte-efficiency/unused-javascript.js
@@ -11,11 +11,11 @@ const JsBundles = require('../../computed/js-bundles.js');
 const i18n = require('../../lib/i18n/i18n.js');
 
 const UIStrings = {
-  /** Imperative title of a Lighthouse audit that tells the user to remove JavaScript that is never evaluated during page load. This is displayed in a list of audit titles that Lighthouse generates. */
-  title: 'Remove unused JavaScript',
-  /** Description of a Lighthouse audit that tells the user *why* they should remove JavaScript that is never needed/evaluated by the browser. This is displayed after a user expands the section to see more. No character length limits. 'Learn More' becomes link text to additional documentation. */
-  description: 'Remove unused JavaScript to reduce bytes consumed by network activity. ' +
-    '[Learn more](https://web.dev/unused-javascript/).',
+  /** Imperative title of a Lighthouse audit that tells the user to reduce JavaScript that is never evaluated during page load. This is displayed in a list of audit titles that Lighthouse generates. */
+  title: 'Reduce unused JavaScript',
+  /** Description of a Lighthouse audit that tells the user *why* they should reduce JavaScript that is never needed/evaluated by the browser. This is displayed after a user expands the section to see more. No character length limits. 'Learn More' becomes link text to additional documentation. */
+  description: 'Reduce unused JavaScript and defer loading scripts until they are required to ' +
+    'decrease bytes consumed by network activity. [Learn more](https://web.dev/unused-javascript/).',
 };
 
 const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
diff --git a/lighthouse-core/gather/driver.js b/lighthouse-core/gather/driver.js
index e0227abfd..33a88fb4b 100644
--- a/lighthouse-core/gather/driver.js
+++ b/lighthouse-core/gather/driver.js
@@ -899,15 +899,6 @@ class Driver {
     `);
   }
 
-  /**
-   * Install a performance observer that watches longtask timestamps for waitForCPUIdle.
-   * @return {Promise<void>}
-   */
-  async registerPerformanceObserver() {
-    const scriptStr = `(${pageFunctions.registerPerformanceObserverInPageString})()`;
-    await this.evaluateScriptOnNewDocument(scriptStr);
-  }
-
   /**
    * Use a RequestIdleCallback shim for tests run with simulated throttling, so that the deadline can be used without
    * a penalty
diff --git a/lighthouse-core/gather/driver/wait-for-condition.js b/lighthouse-core/gather/driver/wait-for-condition.js
index 81964c33e..bf7876e29 100644
--- a/lighthouse-core/gather/driver/wait-for-condition.js
+++ b/lighthouse-core/gather/driver/wait-for-condition.js
@@ -5,10 +5,11 @@
  */
 'use strict';
 
+/* global window */
+
 const log = require('lighthouse-logger');
 const LHError = require('../../lib/lh-error.js');
 const ExecutionContext = require('./execution-context.js');
-const pageFunctions = require('../../lib/page-functions.js');
 
 /** @typedef {import('./network-monitor.js')} NetworkMonitor */
 /** @typedef {import('./network-monitor.js').NetworkMonitorEvent} NetworkMonitorEvent */
@@ -221,7 +222,7 @@ function waitForCPUIdle(session, waitForCPUQuiet) {
   async function checkForQuiet(executionContext, resolve) {
     if (canceled) return;
     const timeSinceLongTask =
-      await executionContext.evaluate(pageFunctions.checkTimeSinceLastLongTask, {args: []});
+      await executionContext.evaluate(checkTimeSinceLastLongTaskInPage, {args: []});
     if (canceled) return;
 
     if (typeof timeSinceLongTask === 'number') {
@@ -241,9 +242,12 @@ function waitForCPUIdle(session, waitForCPUQuiet) {
     throw new Error('waitForCPUIdle.cancel() called before it was defined');
   };
 
+  const executionContext = new ExecutionContext(session);
   /** @type {Promise<void>} */
   const promise = new Promise((resolve, reject) => {
-    checkForQuiet(new ExecutionContext(session), resolve).catch(reject);
+    executionContext.evaluate(registerPerformanceObserverInPage, {args: []})
+      .then(() => checkForQuiet(executionContext, resolve))
+      .catch(reject);
     cancel = () => {
       if (canceled) return;
       canceled = true;
@@ -258,6 +262,69 @@ function waitForCPUIdle(session, waitForCPUQuiet) {
   };
 }
 
+/* c8 ignore start */
+
+/**
+ * This function is executed in the page itself when the document is first loaded.
+ *
+ * Used by _waitForCPUIdle and executed in the context of the page, updates the ____lastLongTask
+ * property on window to the end time of the last long task.
+ */
+function registerPerformanceObserverInPage() {
+  // Do not re-register if we've already run this script.
+  if (window.____lastLongTask !== undefined) return;
+
+  window.____lastLongTask = window.__perfNow();
+  const observer = new window.PerformanceObserver(entryList => {
+    const entries = entryList.getEntries();
+    for (const entry of entries) {
+      if (entry.entryType === 'longtask') {
+        const taskEnd = entry.startTime + entry.duration;
+        window.____lastLongTask = Math.max(window.____lastLongTask || 0, taskEnd);
+      }
+    }
+  });
+
+  observer.observe({entryTypes: ['longtask'], buffered: true});
+}
+
+/**
+ * This function is executed in the page itself.
+ *
+ * Used by _waitForCPUIdle and executed in the context of the page, returns time since last long task.
+ * @return {Promise<number>}
+ */
+function checkTimeSinceLastLongTaskInPage() {
+  // This function attempts to return the time since the last long task occurred.
+  // `PerformanceObserver`s don't always immediately fire though, so we check twice with some time in
+  // between to make sure nothing has happened very recently.
+
+  // Chrome 88 introduced heavy throttling of timers which means our `setTimeout` will be executed
+  // at some point farish (several hundred ms) into the future and the time at which it executes isn't
+  // a reliable indicator of long task existence, instead we check if any information has changed.
+  // See https://developer.chrome.com/blog/timer-throttling-in-chrome-88/
+  return new window.__nativePromise(resolve => {
+    const firstAttemptTs = window.__perfNow();
+    const firstAttemptLastLongTaskTs = window.____lastLongTask || 0;
+
+    setTimeout(() => {
+      // We can't be sure a long task hasn't occurred since our first attempt, but if the `____lastLongTask`
+      // value is the same (i.e. the perf observer didn't have any new information), we can be pretty
+      // confident that the long task info was accurate *at the time of our first attempt*.
+      const secondAttemptLastLongTaskTs = window.____lastLongTask || 0;
+      const timeSinceLongTask =
+        firstAttemptLastLongTaskTs === secondAttemptLastLongTaskTs
+          ? // The time of the last long task hasn't changed, the information from our first attempt is accurate.
+            firstAttemptTs - firstAttemptLastLongTaskTs
+          : // The time of the last long task *did* change, we can't really trust the information we have.
+            0;
+      resolve(timeSinceLongTask);
+    }, 150);
+  });
+}
+
+/* c8 ignore stop */
+
 /**
  * Return a promise that resolves `pauseAfterLoadMs` after the load event
  * fires and a method to cancel internal listeners and timeout.
@@ -294,7 +361,6 @@ function waitForLoadEvent(session, pauseAfterLoadMs) {
   };
 }
 
-
 /**
  * Returns whether the page appears to be hung.
  * @param {LH.Gatherer.FRProtocolSession} session
diff --git a/lighthouse-core/gather/fetcher.js b/lighthouse-core/gather/fetcher.js
index de4a17615..04cb48c9f 100644
--- a/lighthouse-core/gather/fetcher.js
+++ b/lighthouse-core/gather/fetcher.js
@@ -175,12 +175,25 @@ class Fetcher {
       requestInterceptionPromise,
     ]).finally(() => clearTimeout(timeoutHandle));
 
+    // Temporarily disable auto-attaching for this iframe.
+    await this.driver.sendCommand('Target.setAutoAttach', {
+      autoAttach: false,
+      waitForDebuggerOnStart: false,
+    });
+
     const injectionPromise = this.driver.executionContext.evaluate(injectIframe, {
       args: [url],
       useIsolation: true,
     });
 
     const [fetchResult] = await Promise.all([racePromise, injectionPromise]);
+
+    await this.driver.sendCommand('Target.setAutoAttach', {
+      flatten: true,
+      autoAttach: true,
+      waitForDebuggerOnStart: true,
+    });
+
     return fetchResult;
   }
 }
diff --git a/lighthouse-core/gather/gather-runner.js b/lighthouse-core/gather/gather-runner.js
index a9563fe58..cca8a8e07 100644
--- a/lighthouse-core/gather/gather-runner.js
+++ b/lighthouse-core/gather/gather-runner.js
@@ -133,7 +133,6 @@ class GatherRunner {
     await driver.enableRuntimeEvents();
     await driver.enableAsyncStacks();
     await driver.cacheNatives();
-    await driver.registerPerformanceObserver();
     await driver.dismissJavaScriptDialogs();
     await driver.registerRequestIdleCallbackWrap(options.settings);
     if (resetStorage) {
diff --git a/lighthouse-core/lib/i18n/locales/en-US.json b/lighthouse-core/lib/i18n/locales/en-US.json
index b82b3815a..f6958b62d 100644
--- a/lighthouse-core/lib/i18n/locales/en-US.json
+++ b/lighthouse-core/lib/i18n/locales/en-US.json
@@ -513,16 +513,16 @@
     "message": "Minify JavaScript"
   },
   "lighthouse-core/audits/byte-efficiency/unused-css-rules.js | description": {
-    "message": "Remove dead rules from stylesheets and defer the loading of CSS not used for above-the-fold content to reduce unnecessary bytes consumed by network activity. [Learn more](https://web.dev/unused-css-rules/)."
+    "message": "Reduce unused rules from stylesheets and defer CSS not used for above-the-fold content to decrease bytes consumed by network activity. [Learn more](https://web.dev/unused-css-rules/)."
   },
   "lighthouse-core/audits/byte-efficiency/unused-css-rules.js | title": {
-    "message": "Remove unused CSS"
+    "message": "Reduce unused CSS"
   },
   "lighthouse-core/audits/byte-efficiency/unused-javascript.js | description": {
-    "message": "Remove unused JavaScript to reduce bytes consumed by network activity. [Learn more](https://web.dev/unused-javascript/)."
+    "message": "Reduce unused JavaScript and defer loading scripts until they are required to decrease bytes consumed by network activity. [Learn more](https://web.dev/unused-javascript/)."
   },
   "lighthouse-core/audits/byte-efficiency/unused-javascript.js | title": {
-    "message": "Remove unused JavaScript"
+    "message": "Reduce unused JavaScript"
   },
   "lighthouse-core/audits/byte-efficiency/uses-long-cache-ttl.js | description": {
     "message": "A long cache lifetime can speed up repeat visits to your page. [Learn more](https://web.dev/uses-long-cache-ttl/)."
diff --git a/lighthouse-core/lib/i18n/locales/en-XL.json b/lighthouse-core/lib/i18n/locales/en-XL.json
index 8827641d5..d95db3b11 100644
--- a/lighthouse-core/lib/i18n/locales/en-XL.json
+++ b/lighthouse-core/lib/i18n/locales/en-XL.json
@@ -513,16 +513,16 @@
     "message": "M̂ín̂íf̂ý Ĵáv̂áŜćr̂íp̂t́"
   },
   "lighthouse-core/audits/byte-efficiency/unused-css-rules.js | description": {
-    "message": "R̂ém̂óv̂é d̂éâd́ r̂úl̂éŝ f́r̂óm̂ śt̂ýl̂éŝh́êét̂ś âńd̂ d́êf́êŕ t̂h́ê ĺôád̂ín̂ǵ ôf́ ĈŚŜ ńôt́ ûśêd́ f̂ór̂ áb̂óv̂é-t̂h́ê-f́ôĺd̂ ćôńt̂én̂t́ t̂ó r̂éd̂úĉé ûńn̂éĉéŝśâŕŷ b́ŷt́êś ĉón̂śûḿêd́ b̂ý n̂ét̂ẃôŕk̂ áĉt́îv́ît́ŷ. [Ĺêár̂ń m̂ór̂é](https://web.dev/unused-css-rules/)."
+    "message": "R̂éd̂úĉé ûńûśêd́ r̂úl̂éŝ f́r̂óm̂ śt̂ýl̂éŝh́êét̂ś âńd̂ d́êf́êŕ ĈŚŜ ńôt́ ûśêd́ f̂ór̂ áb̂óv̂é-t̂h́ê-f́ôĺd̂ ćôńt̂én̂t́ t̂ó d̂éĉŕêáŝé b̂ýt̂éŝ ćôńŝúm̂éd̂ b́ŷ ńêt́ŵór̂ḱ âćt̂ív̂ít̂ý. [L̂éâŕn̂ ḿôŕê](https://web.dev/unused-css-rules/)."
   },
   "lighthouse-core/audits/byte-efficiency/unused-css-rules.js | title": {
-    "message": "R̂ém̂óv̂é ûńûśêd́ ĈŚŜ"
+    "message": "R̂éd̂úĉé ûńûśêd́ ĈŚŜ"
   },
   "lighthouse-core/audits/byte-efficiency/unused-javascript.js | description": {
-    "message": "R̂ém̂óv̂é ûńûśêd́ Ĵáv̂áŜćr̂íp̂t́ t̂ó r̂éd̂úĉé b̂ýt̂éŝ ćôńŝúm̂éd̂ b́ŷ ńêt́ŵór̂ḱ âćt̂ív̂ít̂ý. [L̂éâŕn̂ ḿôŕê](https://web.dev/unused-javascript/)."
+    "message": "R̂éd̂úĉé ûńûśêd́ Ĵáv̂áŜćr̂íp̂t́ âńd̂ d́êf́êŕ l̂óâd́îńĝ śĉŕîṕt̂ś ûńt̂íl̂ t́ĥéŷ ár̂é r̂éq̂úîŕêd́ t̂ó d̂éĉŕêáŝé b̂ýt̂éŝ ćôńŝúm̂éd̂ b́ŷ ńêt́ŵór̂ḱ âćt̂ív̂ít̂ý. [L̂éâŕn̂ ḿôŕê](https://web.dev/unused-javascript/)."
   },
   "lighthouse-core/audits/byte-efficiency/unused-javascript.js | title": {
-    "message": "R̂ém̂óv̂é ûńûśêd́ Ĵáv̂áŜćr̂íp̂t́"
+    "message": "R̂éd̂úĉé ûńûśêd́ Ĵáv̂áŜćr̂íp̂t́"
   },
   "lighthouse-core/audits/byte-efficiency/uses-long-cache-ttl.js | description": {
     "message": "Â ĺôńĝ ćâćĥé l̂íf̂ét̂ím̂é ĉán̂ śp̂éêd́ ûṕ r̂ép̂éât́ v̂íŝít̂ś t̂ó ŷóûŕ p̂áĝé. [L̂éâŕn̂ ḿôŕê](https://web.dev/uses-long-cache-ttl/)."
diff --git a/lighthouse-core/lib/page-functions.js b/lighthouse-core/lib/page-functions.js
index fb90d2130..8052f7038 100644
--- a/lighthouse-core/lib/page-functions.js
+++ b/lighthouse-core/lib/page-functions.js
@@ -47,57 +47,6 @@ function wrapRuntimeEvalErrorInBrowser(err) {
   };
 }
 
-/**
- * Used by _waitForCPUIdle and executed in the context of the page, updates the ____lastLongTask
- * property on window to the end time of the last long task.
- */
-function registerPerformanceObserverInPage() {
-  window.____lastLongTask = window.__perfNow();
-  const observer = new window.PerformanceObserver(entryList => {
-    const entries = entryList.getEntries();
-    for (const entry of entries) {
-      if (entry.entryType === 'longtask') {
-        const taskEnd = entry.startTime + entry.duration;
-        window.____lastLongTask = Math.max(window.____lastLongTask || 0, taskEnd);
-      }
-    }
-  });
-
-  observer.observe({entryTypes: ['longtask']});
-}
-
-/**
- * Used by _waitForCPUIdle and executed in the context of the page, returns time since last long task.
- * @return {Promise<number>}
- */
-function checkTimeSinceLastLongTask() {
-  // This function attempts to return the time since the last long task occurred.
-  // `PerformanceObserver`s don't always immediately fire though, so we check twice with some time in
-  // between to make sure nothing has happened very recently.
-
-  // Chrome 88 introduced heavy throttling of timers which means our `setTimeout` will be executed
-  // at some point farish (several hundred ms) into the future and the time at which it executes isn't
-  // a reliable indicator of long task existence, instead we check if any information has changed.
-  // See https://developer.chrome.com/blog/timer-throttling-in-chrome-88/
-  return new window.__nativePromise(resolve => {
-    const firstAttemptTs = window.__perfNow();
-    const firstAttemptLastLongTaskTs = window.____lastLongTask || 0;
-
-    setTimeout(() => {
-      // We can't be sure a long task hasn't occurred since our first attempt, but if the `____lastLongTask`
-      // value is the same (i.e. the perf observer didn't have any new information), we can be pretty
-      // confident that the long task info was accurate *at the time of our first attempt*.
-      const secondAttemptLastLongTaskTs = window.____lastLongTask || 0;
-      const timeSinceLongTask = firstAttemptLastLongTaskTs === secondAttemptLastLongTaskTs ?
-        // The time of the last long task hasn't changed, the information from our first attempt is accurate.
-        firstAttemptTs - firstAttemptLastLongTaskTs :
-        // The time of the last long task *did* change, we can't really trust the information we have.
-        0;
-      resolve(timeSinceLongTask);
-    }, 150);
-  });
-}
-
 /**
  * @template {string} T
  * @param {T} selector Optional simple CSS selector to filter nodes on.
@@ -577,8 +526,6 @@ const getNodeDetailsString = `function getNodeDetails(element) {
 module.exports = {
   wrapRuntimeEvalErrorInBrowserString: wrapRuntimeEvalErrorInBrowser.toString(),
   wrapRuntimeEvalErrorInBrowser,
-  registerPerformanceObserverInPageString: registerPerformanceObserverInPage.toString(),
-  checkTimeSinceLastLongTask,
   getElementsInDocument,
   getElementsInDocumentString: getElementsInDocument.toString(),
   getOuterHTMLSnippetString: getOuterHTMLSnippet.toString(),
diff --git a/lighthouse-core/report/html/report-styles.css b/lighthouse-core/report/html/report-styles.css
index 44dc69edc..288b3f696 100644
--- a/lighthouse-core/report/html/report-styles.css
+++ b/lighthouse-core/report/html/report-styles.css
@@ -541,16 +541,18 @@
   background-color: var(--color-hover);
 }
 
-/* Hide the expandable arrow icon, three ways: via the CSS Counter Styles spec, for webkit/blink browsers, hiding the polyfilled icon */
-/* https://github.com/javan/details-element-polyfill/blob/master/src/details-element-polyfill/polyfill.sass */
+/* We want to hide the browser's default arrow marker on summary elements. Admittedly, it's complicated. */
 .lh-audit-group > summary,
 .lh-expandable-details > summary {
-  list-style-type: none;
+  /* Blink 89+ and Firefox will hide the arrow when display is changed from (new) default of `list-item` to block.  https://chromestatus.com/feature/6730096436051968*/
+  display: block;
 }
+/* Safari and Blink <=88 require using the -webkit-details-marker selector */
 .lh-audit-group > summary::-webkit-details-marker,
 .lh-expandable-details > summary::-webkit-details-marker {
   display: none;
 }
+/* When the details-element-polyfill is used, it adds an ugly arrow. Hide it. https://github.com/javan/details-element-polyfill/blob/main/src/styles.js */
 .lh-audit-group > summary:before,
 .lh-expandable-details > summary:before {
   display: none;
diff --git a/lighthouse-core/scripts/pptr-run-devtools.js b/lighthouse-core/scripts/pptr-run-devtools.js
new file mode 100644
index 000000000..578f17870
--- /dev/null
+++ b/lighthouse-core/scripts/pptr-run-devtools.js
@@ -0,0 +1,191 @@
+/**
+ * @license Copyright 2021 The Lighthouse Authors. 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';
+
+/**
+ * USAGE:
+ * Make sure CHROME_PATH is set to a modern version of Chrome.
+ * This script won't work on older versions that use the "Audits" panel.
+ *
+ * URL list file: yarn run-devtools < path/to/urls.txt
+ * Single URL: yarn run-devtools "https://example.com"
+ */
+
+const puppeteer = require('puppeteer');
+const fs = require('fs');
+const readline = require('readline');
+const yargs = require('yargs/yargs');
+
+/** @typedef {{result?: {value?: string, objectId?: number}, exceptionDetails?: object}} RuntimeEvaluateResponse */
+
+const argv = yargs(process.argv.slice(2))
+  .usage('$0 [url]')
+  .help('help').alias('help', 'h')
+  .option('_', {type: 'string'})
+  .option('output-dir', {
+    type: 'string',
+    default: 'latest-run/devtools-lhrs',
+    alias: 'o',
+  })
+  .option('custom-devtools-frontend', {
+    type: 'string',
+    alias: 'd',
+  })
+  .argv;
+
+/**
+ * https://source.chromium.org/chromium/chromium/src/+/master:third_party/devtools-frontend/src/front_end/test_runner/TestRunner.js;l=170;drc=f59e6de269f4f50bca824f8ca678d5906c7d3dc8
+ * @param {Record<string, function>} receiver
+ * @param {string} methodName
+ * @param {function} override
+ */
+function addSniffer(receiver, methodName, override) {
+  const original = receiver[methodName];
+  if (typeof original !== 'function') {
+    throw new Error('Cannot find method to override: ' + methodName);
+  }
+
+  /**
+   * @param  {...any} args
+   */
+  receiver[methodName] = function(...args) {
+    let result;
+    try {
+      result = original.apply(this, args);
+    } finally {
+      receiver[methodName] = original;
+    }
+    // In case of exception the override won't be called.
+    try {
+      Array.prototype.push.call(args, result);
+      override.apply(this, args);
+    } catch (e) {
+      throw new Error('Exception in overriden method \'' + methodName + '\': ' + e);
+    }
+    return result;
+  };
+}
+
+const sniffLhr = `
+new Promise(resolve => {
+  (${addSniffer.toString()})(
+    UI.panels.lighthouse.__proto__,
+    '_buildReportUI',
+    (lhr, artifacts) => resolve(lhr)
+  );
+});
+`;
+
+const startLighthouse = `
+(async () => {
+  const ViewManager = UI.ViewManager.ViewManager || UI.ViewManager;
+  await ViewManager.instance().showView('lighthouse');
+  const button = UI.panels.lighthouse.contentElement.querySelector('button');
+  if (button.disabled) throw new Error('Start button disabled');
+  button.click();
+})()
+`;
+
+/**
+ * @param {import('puppeteer').Browser} browser
+ * @param {string} url
+ * @return {Promise<string>}
+ */
+async function testPage(browser, url) {
+  const page = await browser.newPage();
+
+  const targets = await browser.targets();
+  const inspectorTarget = targets.filter(t => t.url().includes('devtools'))[1];
+  if (!inspectorTarget) throw new Error('No inspector found.');
+
+  const session = await inspectorTarget.createCDPSession();
+  await session.send('Runtime.enable');
+
+  // Navigate to page and wait for initial HTML to be parsed before trying to start LH.
+  await new Promise((resolve, reject) => {
+    page.target().createCDPSession()
+      .then(session => {
+        session.send('Page.enable').then(() => {
+          session.once('Page.domContentEventFired', resolve);
+          page.goto(url);
+        });
+      })
+      .catch(reject);
+  });
+
+  /** @type {RuntimeEvaluateResponse|undefined} */
+  let startLHResponse;
+  while (!startLHResponse || startLHResponse.exceptionDetails) {
+    startLHResponse = await session.send('Runtime.evaluate', {
+      expression: startLighthouse,
+      awaitPromise: true,
+    }).catch(err => ({exceptionDetails: err}));
+  }
+
+  /** @type {RuntimeEvaluateResponse} */
+  const remoteLhrResponse = await session.send('Runtime.evaluate', {
+    expression: sniffLhr,
+    awaitPromise: true,
+    returnByValue: true,
+  }).catch(err => err);
+
+  if (!remoteLhrResponse.result || !remoteLhrResponse.result.value) {
+    throw new Error('Problem sniffing LHR.');
+  }
+
+  await page.close();
+
+  return JSON.stringify(remoteLhrResponse.result.value);
+}
+
+/**
+ * @return {Promise<string[]>}
+ */
+async function readUrlList() {
+  if (argv._[0]) return [argv._[0]];
+
+  /** @type {string[]} */
+  const urlList = [];
+  const rl = readline.createInterface(process.stdin, process.stdout);
+
+  rl.on('line', async line => {
+    if (line.startsWith('#')) return;
+    urlList.push(line);
+  });
+
+  return new Promise(resolve => rl.on('close', () => resolve(urlList)));
+}
+
+async function run() {
+  const outputDir = argv['output-dir'];
+
+  // Create output directory.
+  if (fs.existsSync(outputDir)) {
+    if (fs.readdirSync(outputDir).length) {
+      // eslint-disable-next-line no-console
+      console.warn('WARNING: Output directory is not empty.');
+    }
+  } else {
+    fs.mkdirSync(outputDir);
+  }
+
+  const customDevtools = argv['custom-devtools-frontend'];
+
+  const browser = await puppeteer.launch({
+    executablePath: process.env.CHROME_PATH,
+    args: customDevtools ? [`--custom-devtools-frontend=${customDevtools}`] : [],
+    devtools: true,
+  });
+
+  const urlList = await readUrlList();
+  for (let i = 0; i < urlList.length; ++i) {
+    const lhr = await testPage(browser, urlList[i]);
+    fs.writeFileSync(`${argv.o}/lhr-${i}.json`, lhr);
+  }
+
+  await browser.close();
+}
+run();
diff --git a/lighthouse-core/scripts/roll-to-devtools.sh b/lighthouse-core/scripts/roll-to-devtools.sh
index 4da513604..170c7aa43 100755
--- a/lighthouse-core/scripts/roll-to-devtools.sh
+++ b/lighthouse-core/scripts/roll-to-devtools.sh
@@ -25,7 +25,7 @@ else
   dt_dir="$HOME/src/devtools/devtools-frontend"
 fi
 
-if [[ ! -d "$dt_dir" || ! -a "$dt_dir/front_end/shell.js" ]]; then
+if [[ ! -d "$dt_dir" || ! -a "$dt_dir/front_end/OWNERS" ]]; then
   echo -e "\033[31m✖ Error!\033[39m"
   echo "This script requires a devtools frontend folder. We didn't find one here:"
   echo "    $dt_dir"
diff --git a/lighthouse-treemap/app/index.html b/lighthouse-treemap/app/index.html
index cdc3858be..f169fabfa 100644
--- a/lighthouse-treemap/app/index.html
+++ b/lighthouse-treemap/app/index.html
@@ -17,59 +17,48 @@ Unless required by applicable law or agreed to in writing, software distributed
   <link rel="stylesheet" href="styles/bundled.css">
 </head>
 
-<body>
+<body class="vars">
   <main class="lh-main">
     <div class="lh-settings">
       <header>
         <div class="lh-header--section">
-          <style>
-            .lh-topbar__logo {
-              width: 24px;
-              height: 24px;
-              user-select: none;
-              flex: none;
-            }
-          </style>
+          <!-- Lighthouse logo. Stolen from templates.html -->
+          <svg class="lh-topbar__logo" viewBox="0 0 24 24">
+            <defs>
+              <linearGradient x1="57.456%" y1="13.086%" x2="18.259%" y2="72.322%" id="lh-topbar__logo--a">
+                <stop stop-color="#262626" stop-opacity=".1" offset="0%"/>
+                <stop stop-color="#262626" stop-opacity="0" offset="100%"/>
+              </linearGradient>
+              <linearGradient x1="100%" y1="50%" x2="0%" y2="50%" id="lh-topbar__logo--b">
+                <stop stop-color="#262626" stop-opacity=".1" offset="0%"/>
+                <stop stop-color="#262626" stop-opacity="0" offset="100%"/>
+              </linearGradient>
+              <linearGradient x1="58.764%" y1="65.756%" x2="36.939%" y2="50.14%" id="lh-topbar__logo--c">
+                <stop stop-color="#262626" stop-opacity=".1" offset="0%"/>
+                <stop stop-color="#262626" stop-opacity="0" offset="100%"/>
+              </linearGradient>
+              <linearGradient x1="41.635%" y1="20.358%" x2="72.863%" y2="85.424%" id="lh-topbar__logo--d">
+                <stop stop-color="#FFF" stop-opacity=".1" offset="0%"/>
+                <stop stop-color="#FFF" stop-opacity="0" offset="100%"/>
+              </linearGradient>
+            </defs>
+            <g fill="none" fill-rule="evenodd">
+              <path d="M12 3l4.125 2.625v3.75H18v2.25h-1.688l1.5 9.375H6.188l1.5-9.375H6v-2.25h1.875V5.648L12 3zm2.201 9.938L9.54 14.633 9 18.028l5.625-2.062-.424-3.028zM12.005 5.67l-1.88 1.207v2.498h3.75V6.86l-1.87-1.19z" fill="#F44B21"/>
+              <path fill="#FFF" d="M14.201 12.938L9.54 14.633 9 18.028l5.625-2.062z"/>
+              <path d="M6 18c-2.042 0-3.95-.01-5.813 0l1.5-9.375h4.326L6 18z" fill="url(#lh-topbar__logo--a)" fill-rule="nonzero" transform="translate(6 3)"/>
+              <path fill="#FFF176" fill-rule="nonzero" d="M13.875 9.375v-2.56l-1.87-1.19-1.88 1.207v2.543z"/>
+              <path fill="url(#lh-topbar__logo--b)" fill-rule="nonzero" d="M0 6.375h6v2.25H0z" transform="translate(6 3)"/>
+              <path fill="url(#lh-topbar__logo--c)" fill-rule="nonzero" d="M6 6.375H1.875v-3.75L6 0z" transform="translate(6 3)"/>
+              <path fill="url(#lh-topbar__logo--d)" fill-rule="nonzero" d="M6 0l4.125 2.625v3.75H12v2.25h-1.688l1.5 9.375H.188l1.5-9.375H0v-2.25h1.875V2.648z" transform="translate(6 3)"/>
+            </g>
+          </svg>
 
-          <div>
-            <!-- Lighthouse logo. Stolen from templates.html -->
-            <svg class="lh-topbar__logo" viewBox="0 0 24 24">
-              <defs>
-                <linearGradient x1="57.456%" y1="13.086%" x2="18.259%" y2="72.322%" id="lh-topbar__logo--a">
-                  <stop stop-color="#262626" stop-opacity=".1" offset="0%"/>
-                  <stop stop-color="#262626" stop-opacity="0" offset="100%"/>
-                </linearGradient>
-                <linearGradient x1="100%" y1="50%" x2="0%" y2="50%" id="lh-topbar__logo--b">
-                  <stop stop-color="#262626" stop-opacity=".1" offset="0%"/>
-                  <stop stop-color="#262626" stop-opacity="0" offset="100%"/>
-                </linearGradient>
-                <linearGradient x1="58.764%" y1="65.756%" x2="36.939%" y2="50.14%" id="lh-topbar__logo--c">
-                  <stop stop-color="#262626" stop-opacity=".1" offset="0%"/>
-                  <stop stop-color="#262626" stop-opacity="0" offset="100%"/>
-                </linearGradient>
-                <linearGradient x1="41.635%" y1="20.358%" x2="72.863%" y2="85.424%" id="lh-topbar__logo--d">
-                  <stop stop-color="#FFF" stop-opacity=".1" offset="0%"/>
-                  <stop stop-color="#FFF" stop-opacity="0" offset="100%"/>
-                </linearGradient>
-              </defs>
-              <g fill="none" fill-rule="evenodd">
-                <path d="M12 3l4.125 2.625v3.75H18v2.25h-1.688l1.5 9.375H6.188l1.5-9.375H6v-2.25h1.875V5.648L12 3zm2.201 9.938L9.54 14.633 9 18.028l5.625-2.062-.424-3.028zM12.005 5.67l-1.88 1.207v2.498h3.75V6.86l-1.87-1.19z" fill="#F44B21"/>
-                <path fill="#FFF" d="M14.201 12.938L9.54 14.633 9 18.028l5.625-2.062z"/>
-                <path d="M6 18c-2.042 0-3.95-.01-5.813 0l1.5-9.375h4.326L6 18z" fill="url(#lh-topbar__logo--a)" fill-rule="nonzero" transform="translate(6 3)"/>
-                <path fill="#FFF176" fill-rule="nonzero" d="M13.875 9.375v-2.56l-1.87-1.19-1.88 1.207v2.543z"/>
-                <path fill="url(#lh-topbar__logo--b)" fill-rule="nonzero" d="M0 6.375h6v2.25H0z" transform="translate(6 3)"/>
-                <path fill="url(#lh-topbar__logo--c)" fill-rule="nonzero" d="M6 6.375H1.875v-3.75L6 0z" transform="translate(6 3)"/>
-                <path fill="url(#lh-topbar__logo--d)" fill-rule="nonzero" d="M6 0l4.125 2.625v3.75H12v2.25h-1.688l1.5 9.375H.188l1.5-9.375H0v-2.25h1.875V2.648z" transform="translate(6 3)"/>
-              </g>
-            </svg>
-
-            <span class="lh-header--title lh-text-dim">Lighthouse Treemap</span>
-          </div>
+          <span class="lh-header--title lh-text-dim">Lighthouse Treemap</span>
         </div>
 
         <div class="lh-header--section">
           <span class="lh-header--url-and-size">
-            <a href="" class="lh-header--url" target="_blank" rel="noopener"></a> • <span class="lh-header--size lh-text-dim"></span></span>
+            <a href="" class="lh-header--url" target="_blank" rel="noopener"></a> <span class="lh-text-dim">·</span> <span class="lh-header--size lh-text-dim"></span></span>
           </span>
           <div class="lh-mode-wrapper">
             <div class="lh-modes">
diff --git a/lighthouse-treemap/app/src/main.js b/lighthouse-treemap/app/src/main.js
index 835050066..9c04b11a3 100644
--- a/lighthouse-treemap/app/src/main.js
+++ b/lighthouse-treemap/app/src/main.js
@@ -281,10 +281,9 @@ function renderViewModeButtons(viewModes) {
     const viewModeEl = TreemapUtil.createChildOf(viewModesEl, 'div', 'view-mode');
     viewModeEl.id = `view-mode--${viewMode.id}`;
 
-    const labelEl =
-      TreemapUtil.createChildOf(viewModeEl, 'label', 'view-mode__label');
-    TreemapUtil.createChildOf(labelEl, 'span').textContent = viewMode.label;
-    TreemapUtil.createChildOf(labelEl, 'span', 'lh-text-dim').textContent =
+    const labelEl = TreemapUtil.createChildOf(viewModeEl, 'label');
+    TreemapUtil.createChildOf(labelEl, 'span', 'view-mode__label').textContent = viewMode.label;
+    TreemapUtil.createChildOf(labelEl, 'span', 'view-mode__sublabel lh-text-dim').textContent =
       ` (${viewMode.subLabel})`;
 
     const inputEl = TreemapUtil.createChildOf(labelEl, 'input', 'view-mode__button', {
diff --git a/lighthouse-treemap/app/styles/treemap.css b/lighthouse-treemap/app/styles/treemap.css
index 5ec76b27c..c3d33b588 100644
--- a/lighthouse-treemap/app/styles/treemap.css
+++ b/lighthouse-treemap/app/styles/treemap.css
@@ -4,8 +4,21 @@
  * 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.
  */
 
+.vars {
+  /* Palette using Material Design Colors
+   * https://www.materialui.co/colors */
+  --color-gray-600: #757575;
+  --color-gray-900: #212121;
+
+  --text-color-secondary: var(--color-gray-600);
+  --text-color: var(--color-gray-900);
+  --view-mode-label-color-active: #2a67ce;
+  --view-mode-sublabel-color-active: #4484f3c7;
+}
+
 body {
   font-family: '.SFNSDisplay-Regular', 'Helvetica Neue', 'Lucida Grande', sans-serif;
+  color: var(--text-color);
   margin: 0;
   overflow-y: hidden;
   height: 100vh;
@@ -14,7 +27,7 @@ body {
 }
 
 .lh-text-dim {
-  color: grey;
+  color: var(--text-color-secondary);
 }
 
 .lh-main {
@@ -27,10 +40,12 @@ body {
 
 .lh-header--section {
   display: flex;
-  justify-content: space-between;
   align-items: center;
   font-size: 20px;
 }
+.lh-header--section:nth-of-type(2) {
+  justify-content: space-between;
+}
 .lh-header--url {
   font-weight: bold;
   text-decoration: none;
@@ -39,6 +54,13 @@ body {
   white-space: nowrap;
 }
 
+.lh-topbar__logo {
+  width: 24px;
+  height: 24px;
+  user-select: none;
+  flex: none;
+}
+
 .lh-modes {
   display: flex;
 }
@@ -49,7 +71,8 @@ body {
 
 .view-mode {
   cursor: pointer;
-  border: solid 1px blue;
+  border: solid 1px #e1e2e5;
+  box-shadow: 0 0 0 1px #e1e2e5;
   padding: 5px;
   font-size: 16px;
 }
@@ -62,7 +85,14 @@ body {
   border-bottom-right-radius: 2px;
 }
 .view-mode--active {
-  background-color: lightblue;
+  background-color: #e7f1fe;
+  border-color: #d2e3fc;
+}
+.view-mode--active .view-mode__label {
+  color: var(--view-mode-label-color-active);
+}
+.view-mode--active .view-mode__sublabel {
+  color: var(--view-mode-sublabel-color-active);
 }
 
 .view-mode__button {
@@ -71,7 +101,7 @@ body {
   left: -9999px;
 }
 
-label.view-mode__label {
+.view-mode label {
   cursor: pointer;
 }
 
diff --git a/package.json b/package.json
index 2cd61dfb7..cc674538f 100644
--- a/package.json
+++ b/package.json
@@ -71,7 +71,7 @@
     "update:test-devtools": "bash lighthouse-core/test/chromium-web-tests/test-locally.sh --reset-results",
     "test-devtools": "bash lighthouse-core/test/chromium-web-tests/test-locally.sh",
     "open-devtools": "bash lighthouse-core/scripts/open-devtools.sh",
-    "run-devtools": "bash lighthouse-core/test/chromium-web-tests/test-page-devtools.sh",
+    "run-devtools": "node lighthouse-core/scripts/pptr-run-devtools.js",
     "diff:sample-json": "yarn i18n:checks && bash lighthouse-core/scripts/assert-golden-lhr-unchanged.sh",
     "computeBenchmarkIndex": "./lighthouse-core/scripts/benchmark.js",
     "minify-latest-run": "./lighthouse-core/scripts/lantern/minify-trace.js ./latest-run/defaultPass.trace.json ./latest-run/defaultPass.trace.min.json && ./lighthouse-core/scripts/lantern/minify-devtoolslog.js ./latest-run/defaultPass.devtoolslog.json ./latest-run/defaultPass.devtoolslog.min.json",
diff --git a/third-party/snyk/snapshot.json b/third-party/snyk/snapshot.json
index 1f36ebf61..4f40f148e 100644
--- a/third-party/snyk/snapshot.json
+++ b/third-party/snyk/snapshot.json
@@ -152,7 +152,7 @@
       {"id":"SNYK-JS-THREE-1064931","severity":"high","semver":{"vulnerable":["<0.125.0"]}}
     ],
     "underscore":[
-      {"id":"SNYK-JS-UNDERSCORE-1080984","severity":"medium","semver":{"vulnerable":[">=1.13.0-0 <1.13.0-2",">=1.3.2 <1.12.1"]}}
+      {"id":"SNYK-JS-UNDERSCORE-1080984","severity":"low","semver":{"vulnerable":[">=1.13.0-0 <1.13.0-2",">=1.3.2 <1.12.1"]}}
     ],
     "vue":[
       {"id":"npm:vue:20170829","severity":"medium","semver":{"vulnerable":["<2.4.3"]}},


Test patch:
diff --git a/lighthouse-cli/test/cli/run-test.js b/lighthouse-cli/test/cli/run-test.js
index 89e5acf4a..06c8aae5c 100644
--- a/lighthouse-cli/test/cli/run-test.js
+++ b/lighthouse-cli/test/cli/run-test.js
@@ -12,10 +12,11 @@ const fs = require('fs');
 
 const run = require('../../run.js');
 const parseChromeFlags = require('../../run.js').parseChromeFlags;
-const fastConfig = {
+/** @type {LH.Config.Json} */
+const testConfig = {
   'extends': 'lighthouse:default',
   'settings': {
-    'onlyAudits': ['viewport'],
+    'throttlingMethod': 'devtools',
   },
 };
 
@@ -28,7 +29,7 @@ jest.mock('lighthouse-plugin-simple', () => {
 const getFlags = require('../../cli-flags.js').getFlags;
 
 describe('CLI run', function() {
-  describe('LH round trip', () => {
+  describe('runLighthouse runs Lighthouse as a node module', () => {
     /** @type {LH.RunnerResult} */
     let passedResults;
     const filename = path.join(process.cwd(), 'run.ts.results.json');
@@ -36,14 +37,21 @@ describe('CLI run', function() {
     let fileResults;
 
     beforeAll(async () => {
-      const url = 'chrome://version';
-      const timeoutFlag = `--max-wait-for-load=${9000}`;
-      const pluginsFlag = '--plugins=lighthouse-plugin-simple';
+      const url = 'http://localhost:10200/dobetterweb/dbw_tester.html';
+      // eslint-disable-next-line max-len
+      const samplev2ArtifactsPath = __dirname + '/../../../lighthouse-core/test/results/artifacts/';
 
       // eslint-disable-next-line max-len
-      const flags = getFlags(`--output=json --output-path=${filename} ${pluginsFlag} ${timeoutFlag} ${url}`);
+      const flags = getFlags([
+        '--output=json',
+        `--output-path=${filename}`,
+        '--plugins=lighthouse-plugin-simple',
+        // Use sample artifacts to avoid gathering during a unit test.
+        `--audit-mode=${samplev2ArtifactsPath}`,
+        url,
+      ].join(' '));
 
-      const rawResult = await run.runLighthouse(url, flags, fastConfig);
+      const rawResult = await run.runLighthouse(url, flags, testConfig);
 
       if (!rawResult) {
         return assert.fail('no results');
@@ -60,7 +68,7 @@ describe('CLI run', function() {
 
     it('returns results that match the saved results', () => {
       const {lhr} = passedResults;
-      assert.equal(fileResults.audits.viewport.score, 0);
+      assert.equal(fileResults.audits.viewport.score, 1);
 
       // passed results match saved results
       assert.strictEqual(fileResults.fetchTime, lhr.fetchTime);
diff --git a/lighthouse-core/test/chromium-web-tests/test-page-devtools.sh b/lighthouse-core/test/chromium-web-tests/test-page-devtools.sh
deleted file mode 100644
index 3619ff44c..000000000
--- a/lighthouse-core/test/chromium-web-tests/test-page-devtools.sh
+++ /dev/null
@@ -1,87 +0,0 @@
-#!/usr/bin/env bash
-
-##
-# @license Copyright 2020 The Lighthouse Authors. 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.
-##
-
-# USAGE:
-#
-# URL list file: yarn run-devtools < lighthouse-core/scripts/gcp-collection/urls.txt
-#                yarn run-devtools lighthouse-core/scripts/gcp-collection/urls.txt
-# Single URL: echo "https://example.com" | yarn run-devtools
-#
-
-set -euo pipefail
-
-SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
-export LH_ROOT="$( cd "$SCRIPT_DIR/../../.." && pwd )"
-
-# Setup dependencies.
-TEST_DIR="$LH_ROOT/.tmp/chromium-web-tests"
-export DEPOT_TOOLS_PATH="$TEST_DIR/depot-tools"
-export DEVTOOLS_PATH=${DEVTOOLS_PATH:-"$TEST_DIR/devtools/devtools-frontend"}
-export BLINK_TOOLS_PATH="$TEST_DIR/blink_tools"
-export PATH=$DEPOT_TOOLS_PATH:$PATH
-
-bash "$SCRIPT_DIR/download-depot-tools.sh"
-bash "$SCRIPT_DIR/download-devtools.sh"
-bash "$SCRIPT_DIR/download-blink-tools.sh"
-bash "$SCRIPT_DIR/download-content-shell.sh"
-
-bash "$SCRIPT_DIR/roll-devtools.sh"
-
-WEB_TEST_DIR="$DEVTOOLS_PATH/test/webtests/http/tests/devtools/lighthouse-run"
-
-# Remove any tests leftover from previous run.
-if [ -d "$WEB_TEST_DIR" ]; then
-  rm -rf "$WEB_TEST_DIR"
-fi
-mkdir -p "$WEB_TEST_DIR"
-
-echo "Creating test files for URLs"
-COUNTER=0
-while read url; do
-  [[ -z $url || ${url:0:1} == "#" ]] && continue
-  echo "
-  (async function() {
-    await TestRunner.navigatePromise('$url');
-
-    await TestRunner.loadModule('lighthouse_test_runner');
-    await TestRunner.showPanel('lighthouse');
-
-    LighthouseTestRunner.getRunButton().click();
-    const {lhr} = await LighthouseTestRunner.waitForResults();
-    TestRunner.addResult(JSON.stringify(lhr));
-
-    TestRunner.completeTest();
-  })();
-  " > "$WEB_TEST_DIR/lighthouse-run-$COUNTER.js"
-  COUNTER=$[$COUNTER +1]
-done <"${1:-/dev/stdin}"
-
-set +e
-bash "$SCRIPT_DIR/web-test-server.sh" \
-  --no-show-results \
-  --no-retry-failures \
-  --time-out-ms=60000 \
-  --additional-driver-flag=--disable-blink-features=TrustTokens,TrustTokensAlwaysAllowIssuance \
-  http/tests/devtools/lighthouse-run
-set -e
-
-OUTPUT_DIR="$LH_ROOT/latest-run/devtools-lhrs" 
-if [ -d "$OUTPUT_DIR" ]; then
-  rm -rf "$OUTPUT_DIR"
-fi
-mkdir -p "$OUTPUT_DIR"
-
-# Copy results to latest-run folder.
-# Sometimes there will be extra output before the line with LHR. To get around this, only copy the last line with content.
-COUNTER=0
-for file in "$LH_ROOT/.tmp/layout-test-results/http/tests/devtools/lighthouse-run"/lighthouse-run-*-actual.txt; do
-  grep "lighthouseVersion" -m 1 "$file" > "$OUTPUT_DIR/devtools-lhr-$COUNTER.json" 
-  COUNTER=$[$COUNTER +1]
-done
-
-echo "Open the LHRs at $OUTPUT_DIR"
diff --git a/lighthouse-core/test/gather/fake-driver.js b/lighthouse-core/test/gather/fake-driver.js
index 999fd7267..555464093 100644
--- a/lighthouse-core/test/gather/fake-driver.js
+++ b/lighthouse-core/test/gather/fake-driver.js
@@ -84,9 +84,6 @@ function makeFakeDriver({protocolGetVersionResponse}) {
     getScrollPosition() {
       return Promise.resolve(scrollPosition);
     },
-    registerPerformanceObserver() {
-      return Promise.resolve();
-    },
     beginTrace() {
       return Promise.resolve();
     },
diff --git a/lighthouse-core/test/gather/gather-runner-test.js b/lighthouse-core/test/gather/gather-runner-test.js
index b9e3f8883..463667cfe 100644
--- a/lighthouse-core/test/gather/gather-runner-test.js
+++ b/lighthouse-core/test/gather/gather-runner-test.js
@@ -341,7 +341,6 @@ describe('GatherRunner', function() {
       enableAsyncStacks: asyncFunc,
       cacheNatives: asyncFunc,
       gotoURL: asyncFunc,
-      registerPerformanceObserver: asyncFunc,
       registerRequestIdleCallbackWrap: asyncFunc,
       cleanBrowserCaches: createCheck('calledCleanBrowserCaches'),
       clearDataForOrigin: createCheck('calledClearStorage'),
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index d088168ca..822b38c4d 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -3771,8 +3771,8 @@
     },
     "unused-css-rules": {
       "id": "unused-css-rules",
-      "title": "Remove unused CSS",
-      "description": "Remove dead rules from stylesheets and defer the loading of CSS not used for above-the-fold content to reduce unnecessary bytes consumed by network activity. [Learn more](https://web.dev/unused-css-rules/).",
+      "title": "Reduce unused CSS",
+      "description": "Reduce unused rules from stylesheets and defer CSS not used for above-the-fold content to decrease bytes consumed by network activity. [Learn more](https://web.dev/unused-css-rules/).",
       "score": 1,
       "scoreDisplayMode": "numeric",
       "numericValue": 0,
@@ -3788,8 +3788,8 @@
     },
     "unused-javascript": {
       "id": "unused-javascript",
-      "title": "Remove unused JavaScript",
-      "description": "Remove unused JavaScript to reduce bytes consumed by network activity. [Learn more](https://web.dev/unused-javascript/).",
+      "title": "Reduce unused JavaScript",
+      "description": "Reduce unused JavaScript and defer loading scripts until they are required to decrease bytes consumed by network activity. [Learn more](https://web.dev/unused-javascript/).",
       "score": 1,
       "scoreDisplayMode": "numeric",
       "numericValue": 0,
diff --git a/third-party/chromium-webtests/webtests/http/tests/devtools/lighthouse/lighthouse-successful-run-expected.txt b/third-party/chromium-webtests/webtests/http/tests/devtools/lighthouse/lighthouse-successful-run-expected.txt
index 85c09822c..8bdcec4d1 100644
--- a/third-party/chromium-webtests/webtests/http/tests/devtools/lighthouse/lighthouse-successful-run-expected.txt
+++ b/third-party/chromium-webtests/webtests/http/tests/devtools/lighthouse/lighthouse-successful-run-expected.txt
@@ -308,8 +308,8 @@ Computing artifact: FirstContentfulPaint
 Computing artifact: LanternFirstContentfulPaint
 Auditing: Minify CSS
 Auditing: Minify JavaScript
-Auditing: Remove unused CSS
-Auditing: Remove unused JavaScript
+Auditing: Reduce unused CSS
+Auditing: Reduce unused JavaScript
 Computing artifact: JSBundles
 Auditing: Serve images in next-gen formats
 Auditing: Efficiently encode images