Repository: GoogleChrome/lighthouse
Pull request #12576 (2021-05-27 23:45:05)

core(scoring): update TBT score curve
Part of #11866

Updates the Total Blocking Time control points to be closer to the intended methodology laid out in https://web.dev/performance-scoring/#metric-scores  


In addition, if we were picking entirely new control points, the May 2021 HTTP Archive run gives

HA p08 - 19ms 
HA p25 - 189ms

But we think these as our control points would be too jarring and aggressive for now. Still, there's room to improve, so we're doing a small shift of TBT being scored more strictly.

p10: 200
median: 600

Desktop remains unchanged.


Patch:
diff --git a/.github/workflows/devtools.yml b/.github/workflows/devtools.yml
index 94b325d2f..8b277623b 100644
--- a/.github/workflows/devtools.yml
+++ b/.github/workflows/devtools.yml
@@ -33,7 +33,7 @@ jobs:
           ${{ env.BLINK_TOOLS_PATH }}
           ${{ github.workspace }}/lighthouse/.tmp/chromium-web-tests/content-shells
         # The number is how many times this hash key was manually updated to break the cache.
-        key: ${{ runner.os }}-1-${{ hashFiles('third-party/chromium-webtests/webtests/http/tests/devtools/lighthouse/*.js', 'lighthouse/.github/workflows/devtools.yml', 'lighthouse/lighthouse-core/test/chromium-web-tests/*', 'lighthouse/clients/devtools-entry.js', 'lighthouse/clients/devtools-report-assets.js', 'lighthouse/build/build-bundle.js', 'lighthouse/build/build-dt-report-resources.js') }}
+        key: ${{ runner.os }}-2-${{ hashFiles('third-party/chromium-webtests/webtests/http/tests/devtools/lighthouse/*.js', 'lighthouse/.github/workflows/devtools.yml', 'lighthouse/lighthouse-core/test/chromium-web-tests/*', 'lighthouse/clients/devtools-entry.js', 'lighthouse/clients/devtools-report-assets.js', 'lighthouse/build/build-bundle.js', 'lighthouse/build/build-dt-report-resources.js') }}
 
     - name: Use Node.js 12.x
       uses: actions/setup-node@v1
diff --git a/lighthouse-core/audits/metrics/total-blocking-time.js b/lighthouse-core/audits/metrics/total-blocking-time.js
index 43a2126a8..9806feee0 100644
--- a/lighthouse-core/audits/metrics/total-blocking-time.js
+++ b/lighthouse-core/audits/metrics/total-blocking-time.js
@@ -37,14 +37,21 @@ class TotalBlockingTime extends Audit {
   static get defaultOptions() {
     return {
       mobile: {
-        // According to a cluster telemetry run over top 10k sites on mobile, 5th percentile was 0ms,
-        // 25th percentile was 270ms and median was 895ms. These numbers include 404 pages. Picking
-        // thresholds according to our 25/75-th rule will be quite harsh scoring (a single 350ms task)
-        // after FCP will yield a score of .5. The following coefficients are semi-arbitrarily picked
-        // to give 600ms jank a score of .5 and 100ms jank a score of .999. We can tweak these numbers
-        // in the future. See https://www.desmos.com/calculator/bbsv8fedg5
+        // If determined from HTTP Archive data…
+        //     SELECT
+        //         APPROX_QUANTILES(tbt_value, 100)[OFFSET(8)] AS p08_tbt,
+        //         APPROX_QUANTILES(tbt_value, 100)[OFFSET(25)] AS p25_tbt
+        //     FROM (
+        //         SELECT CAST(JSON_EXTRACT_SCALAR(report, '$.audits.total-blocking-time.numericValue') AS FLOAT64) AS tbt_value
+        //         FROM `httparchive.lighthouse.2021_05_01_mobile`
+        //         WHERE report is not NULL
+        //     )
+        // …we'd use control points of 19 and 189, which leads to surprisingly harsh scoring.
+        //
+        // The following coefficients are semi-arbitrarily chosen, but start to approach the "correct" ones:
+        // See https://www.desmos.com/calculator/pwcgna1cvf go/lh8-tbt-curves
         scoring: {
-          p10: 287,
+          p10: 200,
           median: 600,
         },
       },
diff --git a/lighthouse-core/fraggle-rock/config/default-config.js b/lighthouse-core/fraggle-rock/config/default-config.js
index 999213ef3..6d2d961ce 100644
--- a/lighthouse-core/fraggle-rock/config/default-config.js
+++ b/lighthouse-core/fraggle-rock/config/default-config.js
@@ -23,6 +23,7 @@ const artifacts = {
   EmbeddedContent: '',
   FontSize: '',
   FormElements: '',
+  FullPageScreenshot: '',
   GlobalListeners: '',
   IFrameElements: '',
   ImageElements: '',
@@ -71,6 +72,7 @@ const defaultConfig = {
     {id: artifacts.EmbeddedContent, gatherer: 'seo/embedded-content'},
     {id: artifacts.FontSize, gatherer: 'seo/font-size'},
     {id: artifacts.FormElements, gatherer: 'form-elements'},
+    {id: artifacts.FullPageScreenshot, gatherer: 'full-page-screenshot'},
     {id: artifacts.GlobalListeners, gatherer: 'global-listeners'},
     {id: artifacts.IFrameElements, gatherer: 'iframe-elements'},
     {id: artifacts.ImageElements, gatherer: 'image-elements'},
@@ -121,6 +123,7 @@ const defaultConfig = {
         artifacts.EmbeddedContent,
         artifacts.FontSize,
         artifacts.FormElements,
+        artifacts.FullPageScreenshot,
         artifacts.GlobalListeners,
         artifacts.IFrameElements,
         artifacts.ImageElements,
diff --git a/lighthouse-core/fraggle-rock/gather/navigation-runner.js b/lighthouse-core/fraggle-rock/gather/navigation-runner.js
index 824fc9f4b..32a0dcd50 100644
--- a/lighthouse-core/fraggle-rock/gather/navigation-runner.js
+++ b/lighthouse-core/fraggle-rock/gather/navigation-runner.js
@@ -171,6 +171,7 @@ async function _navigation(navigationContext) {
     computedCache: navigationContext.computedCache,
     artifactDefinitions: navigationContext.navigation.artifacts,
     artifactState,
+    settings: navigationContext.config.settings,
   };
 
   const setupResult = await _setupNavigation(navigationContext);
diff --git a/lighthouse-core/fraggle-rock/gather/runner-helpers.js b/lighthouse-core/fraggle-rock/gather/runner-helpers.js
index e3098b12e..07fd927c0 100644
--- a/lighthouse-core/fraggle-rock/gather/runner-helpers.js
+++ b/lighthouse-core/fraggle-rock/gather/runner-helpers.js
@@ -13,6 +13,7 @@
  * @property {LH.Gatherer.FRGatherPhase} phase
  * @property {LH.Gatherer.GatherMode} gatherMode
  * @property {Map<string, LH.ArbitraryEqualityMap>} computedCache
+ * @property {LH.Config.Settings} settings
  */
 
 /** @typedef {Record<string, Promise<any>>} IntermediateArtifacts */
@@ -61,7 +62,15 @@ const phaseToPriorPhase = {
  * @param {CollectPhaseArtifactOptions} options
  */
 async function collectPhaseArtifacts(options) {
-  const {driver, artifactDefinitions, artifactState, phase, gatherMode, computedCache} = options;
+  const {
+    driver,
+    artifactDefinitions,
+    artifactState,
+    phase,
+    gatherMode,
+    computedCache,
+    settings,
+  } = options;
   const priorPhase = phaseToPriorPhase[phase];
   const priorPhaseArtifacts = (priorPhase && artifactState[priorPhase]) || {};
 
@@ -80,6 +89,7 @@ async function collectPhaseArtifacts(options) {
         driver,
         dependencies,
         computedCache,
+        settings,
       });
     });
 
diff --git a/lighthouse-core/fraggle-rock/gather/snapshot-runner.js b/lighthouse-core/fraggle-rock/gather/snapshot-runner.js
index 07831ad44..cbe01bf91 100644
--- a/lighthouse-core/fraggle-rock/gather/snapshot-runner.js
+++ b/lighthouse-core/fraggle-rock/gather/snapshot-runner.js
@@ -40,6 +40,7 @@ async function snapshot(options) {
         artifactDefinitions,
         artifactState,
         computedCache,
+        settings: config.settings,
       });
 
       const artifacts = await awaitArtifacts(artifactState);
diff --git a/lighthouse-core/fraggle-rock/gather/timespan-runner.js b/lighthouse-core/fraggle-rock/gather/timespan-runner.js
index 8207f5f2c..555b2bdca 100644
--- a/lighthouse-core/fraggle-rock/gather/timespan-runner.js
+++ b/lighthouse-core/fraggle-rock/gather/timespan-runner.js
@@ -36,6 +36,7 @@ async function startTimespan(options) {
     artifactState,
     computedCache,
     gatherMode: 'timespan',
+    settings: config.settings,
   };
 
   await collectPhaseArtifacts({phase: 'startInstrumentation', ...phaseOptions});
diff --git a/lighthouse-core/gather/gatherers/full-page-screenshot.js b/lighthouse-core/gather/gatherers/full-page-screenshot.js
index 6170f9297..1bb9a1011 100644
--- a/lighthouse-core/gather/gatherers/full-page-screenshot.js
+++ b/lighthouse-core/gather/gatherers/full-page-screenshot.js
@@ -7,12 +7,10 @@
 
 /* globals window document getBoundingClientRect */
 
-const Gatherer = require('./gatherer.js');
+const FRGatherer = require('../../fraggle-rock/gather/base-gatherer.js');
 const emulation = require('../../lib/emulation.js');
 const pageFunctions = require('../../lib/page-functions.js');
 
-/** @typedef {import('../driver.js')} Driver */
-
 // JPEG quality setting
 // Exploration and examples of reports using different quality settings: https://docs.google.com/document/d/1ZSffucIca9XDW2eEwfoevrk-OTl7WQFeMf0CgeJAA8M/edit#
 const FULL_PAGE_SCREENSHOT_QUALITY = 30;
@@ -24,14 +22,19 @@ function snakeCaseToCamelCase(str) {
   return str.replace(/(-\w)/g, m => m[1].toUpperCase());
 }
 
-class FullPageScreenshot extends Gatherer {
+class FullPageScreenshot extends FRGatherer {
+  /** @type {LH.Gatherer.GathererMeta} */
+  meta = {
+    supportedModes: ['snapshot', 'timespan', 'navigation'],
+  }
+
   /**
-   * @param {Driver} driver
+   * @param {LH.Gatherer.FRTransitionalContext} context
    * @return {Promise<number>}
    * @see https://bugs.chromium.org/p/chromium/issues/detail?id=770769
    */
-  async getMaxScreenshotHeight(driver) {
-    return await driver.executionContext.evaluate(pageFunctions.getMaxTextureSize, {
+  async getMaxScreenshotHeight(context) {
+    return await context.driver.executionContext.evaluate(pageFunctions.getMaxTextureSize, {
       args: [],
       useIsolation: true,
       deps: [],
@@ -39,13 +42,13 @@ class FullPageScreenshot extends Gatherer {
   }
 
   /**
-   * @param {LH.Gatherer.PassContext} passContext
+   * @param {LH.Gatherer.FRTransitionalContext} context
    * @return {Promise<LH.Artifacts.FullPageScreenshot['screenshot']>}
    */
-  async _takeScreenshot(passContext) {
-    const driver = passContext.driver;
-    const maxScreenshotHeight = await this.getMaxScreenshotHeight(driver);
-    const metrics = await driver.sendCommand('Page.getLayoutMetrics');
+  async _takeScreenshot(context) {
+    const session = context.driver.defaultSession;
+    const maxScreenshotHeight = await this.getMaxScreenshotHeight(context);
+    const metrics = await session.sendCommand('Page.getLayoutMetrics');
 
     // Width should match emulated width, without considering content overhang.
     // Both layoutViewport and visualViewport capture this. visualViewport accounts
@@ -56,9 +59,9 @@ class FullPageScreenshot extends Gatherer {
     const width = Math.min(metrics.layoutViewport.clientWidth, maxScreenshotHeight);
     const height = Math.min(metrics.contentSize.height, maxScreenshotHeight);
 
-    await driver.sendCommand('Emulation.setDeviceMetricsOverride', {
+    await session.sendCommand('Emulation.setDeviceMetricsOverride', {
       // If we're gathering with mobile screenEmulation on (overlay scrollbars, etc), continue to use that for this screenshot.
-      mobile: passContext.settings.screenEmulation.mobile,
+      mobile: context.settings.screenEmulation.mobile,
       height,
       width,
       deviceScaleFactor: 1,
@@ -70,7 +73,7 @@ class FullPageScreenshot extends Gatherer {
     // The lower in the page, the more likely (footer elements especially).
     // https://github.com/GoogleChrome/lighthouse/issues/11118
 
-    const result = await driver.sendCommand('Page.captureScreenshot', {
+    const result = await session.sendCommand('Page.captureScreenshot', {
       format: 'jpeg',
       quality: FULL_PAGE_SCREENSHOT_QUALITY,
     });
@@ -90,10 +93,10 @@ class FullPageScreenshot extends Gatherer {
    * `getNodeDetails` maintains a collection of DOM objects in the page, which we can iterate
    * to re-collect the bounding client rectangle.
    * @see pageFunctions.getNodeDetails
-   * @param {LH.Gatherer.PassContext} passContext
+   * @param {LH.Gatherer.FRTransitionalContext} context
    * @return {Promise<LH.Artifacts.FullPageScreenshot['nodes']>}
    */
-  async _resolveNodes(passContext) {
+  async _resolveNodes(context) {
     function resolveNodes() {
       /** @type {LH.Artifacts.FullPageScreenshot['nodes']} */
       const nodes = {};
@@ -113,7 +116,7 @@ class FullPageScreenshot extends Gatherer {
      * @param {{useIsolation: boolean}} _
      */
     function resolveNodesInPage({useIsolation}) {
-      return passContext.driver.executionContext.evaluate(resolveNodes, {
+      return context.driver.executionContext.evaluate(resolveNodes, {
         args: [],
         useIsolation,
         deps: [pageFunctions.getBoundingClientRectString],
@@ -129,26 +132,27 @@ class FullPageScreenshot extends Gatherer {
   }
 
   /**
-   * @param {LH.Gatherer.PassContext} passContext
+   * @param {LH.Gatherer.FRTransitionalContext} context
    * @return {Promise<LH.Artifacts['FullPageScreenshot']>}
    */
-  async afterPass(passContext) {
-    const {driver} = passContext;
-    const executionContext = driver.executionContext;
+  async getArtifact(context) {
+    const session = context.driver.defaultSession;
+    const executionContext = context.driver.executionContext;
+    const settings = context.settings;
 
     // In case some other program is controlling emulation, try to remember what the device looks
     // like now and reset after gatherer is done.
-    const lighthouseControlsEmulation = !passContext.settings.screenEmulation.disabled;
+    const lighthouseControlsEmulation = !settings.screenEmulation.disabled;
 
     try {
       return {
-        screenshot: await this._takeScreenshot(passContext),
-        nodes: await this._resolveNodes(passContext),
+        screenshot: await this._takeScreenshot(context),
+        nodes: await this._resolveNodes(context),
       };
     } finally {
       // Revert resized page.
       if (lighthouseControlsEmulation) {
-        await emulation.emulate(driver.defaultSession, passContext.settings);
+        await emulation.emulate(session, settings);
       } else {
         // Best effort to reset emulation to what it was.
         // https://github.com/GoogleChrome/lighthouse/pull/10716#discussion_r428970681
@@ -179,8 +183,8 @@ class FullPageScreenshot extends Gatherer {
           useIsolation: true,
           deps: [snakeCaseToCamelCase],
         });
-        await driver.sendCommand('Emulation.setDeviceMetricsOverride', {
-          mobile: passContext.settings.formFactor === 'mobile',
+        await session.sendCommand('Emulation.setDeviceMetricsOverride', {
+          mobile: settings.formFactor === 'mobile',
           ...observedDeviceMetrics,
         });
       }
diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts
index 24e7dfc1c..9944615aa 100644
--- a/types/artifacts.d.ts
+++ b/types/artifacts.d.ts
@@ -21,7 +21,6 @@ declare global {
 
     export type FRArtifacts = StrictOmit<Artifacts,
       | 'Fonts'
-      | 'FullPageScreenshot'
       | 'HTTPRedirect'
       | 'Manifest'
       | 'MixedContent'
diff --git a/types/gatherer.d.ts b/types/gatherer.d.ts
index 0e6f9c4a8..7e1a82bdd 100644
--- a/types/gatherer.d.ts
+++ b/types/gatherer.d.ts
@@ -46,6 +46,8 @@ declare global {
       computedCache: Map<string, ArbitraryEqualityMap>;
       /** The set of available dependencies requested by the current gatherer. */
       dependencies: Pick<GathererArtifacts, Exclude<TDependencies, DefaultDependenciesKey>>;
+      /** The settings used for gathering. */
+      settings: Config.Settings;
     }
 
     export interface PassContext {


Test patch:
diff --git a/lighthouse-core/test/audits/metrics/total-blocking-time-test.js b/lighthouse-core/test/audits/metrics/total-blocking-time-test.js
index ab351c410..d051edabd 100644
--- a/lighthouse-core/test/audits/metrics/total-blocking-time-test.js
+++ b/lighthouse-core/test/audits/metrics/total-blocking-time-test.js
@@ -58,7 +58,7 @@ describe('Performance: total-blocking-time audit', () => {
 
     const outputMobile = await TBTAudit.audit(artifactsMobile, contextMobile);
     expect(outputMobile.numericValue).toBeCloseTo(333, 1);
-    expect(outputMobile.score).toBe(0.85);
+    expect(outputMobile.score).toBe(0.75);
     expect(outputMobile.displayValue).toBeDisplayString('330\xa0ms');
 
     const artifactsDesktop = generateArtifacts({trace: lcpTrace,
diff --git a/lighthouse-core/test/fraggle-rock/api-test-pptr.js b/lighthouse-core/test/fraggle-rock/api-test-pptr.js
index d6313e207..960a5ba62 100644
--- a/lighthouse-core/test/fraggle-rock/api-test-pptr.js
+++ b/lighthouse-core/test/fraggle-rock/api-test-pptr.js
@@ -95,7 +95,7 @@ describe('Fraggle Rock API', () => {
 
       const {auditResults, erroredAudits, failedAudits} = getAuditsBreakdown(lhr);
       // TODO(FR-COMPAT): This assertion can be removed when full compatibility is reached.
-      expect(auditResults.length).toMatchInlineSnapshot(`73`);
+      expect(auditResults.length).toMatchInlineSnapshot(`74`);
 
       expect(erroredAudits).toHaveLength(0);
       expect(failedAudits.map(audit => audit.id)).toContain('label');
@@ -121,7 +121,7 @@ describe('Fraggle Rock API', () => {
 
       const {auditResults, erroredAudits, failedAudits} = getAuditsBreakdown(lhr);
       // TODO(FR-COMPAT): This assertion can be removed when full compatibility is reached.
-      expect(auditResults.length).toMatchInlineSnapshot(`27`);
+      expect(auditResults.length).toMatchInlineSnapshot(`28`);
 
       expect(erroredAudits).toHaveLength(0);
       expect(failedAudits.map(audit => audit.id)).toContain('errors-in-console');
@@ -159,7 +159,7 @@ describe('Fraggle Rock API', () => {
       const {lhr} = result;
       const {auditResults, failedAudits, erroredAudits} = getAuditsBreakdown(lhr);
       // TODO(FR-COMPAT): This assertion can be removed when full compatibility is reached.
-      expect(auditResults.length).toMatchInlineSnapshot(`113`);
+      expect(auditResults.length).toMatchInlineSnapshot(`114`);
       expect(erroredAudits).toHaveLength(0);
 
       const failedAuditIds = failedAudits.map(audit => audit.id);
diff --git a/lighthouse-core/test/fraggle-rock/config/config-test.js b/lighthouse-core/test/fraggle-rock/config/config-test.js
index 9875bc004..4d4437657 100644
--- a/lighthouse-core/test/fraggle-rock/config/config-test.js
+++ b/lighthouse-core/test/fraggle-rock/config/config-test.js
@@ -63,8 +63,8 @@ describe('Fraggle Rock Config', () => {
   });
 
   it('should throw on invalid artifact definitions', () => {
-    const configJson = {artifacts: [{id: 'FullPageScreenshot', gatherer: 'full-page-screenshot'}]};
-    expect(() => initializeConfig(configJson, {gatherMode})).toThrow(/FullPageScreenshot gatherer/);
+    const configJson = {artifacts: [{id: 'ScriptElements', gatherer: 'script-elements'}]};
+    expect(() => initializeConfig(configJson, {gatherMode})).toThrow(/ScriptElements gatherer/);
   });
 
   it('should resolve navigation definitions', () => {
diff --git a/lighthouse-core/test/fraggle-rock/gather/mock-driver.js b/lighthouse-core/test/fraggle-rock/gather/mock-driver.js
index 0db3a3f1d..faa9aaf51 100644
--- a/lighthouse-core/test/fraggle-rock/gather/mock-driver.js
+++ b/lighthouse-core/test/fraggle-rock/gather/mock-driver.js
@@ -12,6 +12,7 @@ const {
   createMockOnceFn,
   createMockSendCommandFn,
 } = require('../../gather/mock-commands.js');
+const {defaultSettings} = require('../../../config/constants.js');
 
 /**
  * @fileoverview Mock fraggle rock driver for testing.
@@ -134,6 +135,7 @@ function createMockContext() {
     gatherMode: 'navigation',
     computedCache: new Map(),
     dependencies: {},
+    settings: defaultSettings,
 
     /** @return {LH.Gatherer.FRTransitionalContext} */
     asContext() {
@@ -156,7 +158,10 @@ function mockDriverSubmodules() {
     prepareTargetForIndividualNavigation: jest.fn(),
   };
   const storageMock = {clearDataForOrigin: jest.fn()};
-  const emulationMock = {clearThrottling: jest.fn()};
+  const emulationMock = {
+    clearThrottling: jest.fn(),
+    emulate: jest.fn(),
+  };
   const networkMock = {
     fetchResponseBodyFromCache: jest.fn(),
   };
@@ -167,6 +172,7 @@ function mockDriverSubmodules() {
     prepareMock.prepareTargetForIndividualNavigation = jest.fn().mockResolvedValue({warnings: []});
     storageMock.clearDataForOrigin = jest.fn();
     emulationMock.clearThrottling = jest.fn();
+    emulationMock.emulate = jest.fn();
     networkMock.fetchResponseBodyFromCache = jest.fn().mockResolvedValue('');
   }
 
diff --git a/lighthouse-core/test/fraggle-rock/gather/runner-helpers-test.js b/lighthouse-core/test/fraggle-rock/gather/runner-helpers-test.js
index 6d4208983..3feaf1f38 100644
--- a/lighthouse-core/test/fraggle-rock/gather/runner-helpers-test.js
+++ b/lighthouse-core/test/fraggle-rock/gather/runner-helpers-test.js
@@ -7,6 +7,7 @@
 
 const helpers = require('../../../fraggle-rock/gather/runner-helpers.js');
 const Gatherer = require('../../../fraggle-rock/gather/base-gatherer.js');
+const {defaultSettings} = require('../../../config/constants.js');
 const {createMockDriver, createMockGathererInstance} = require('./mock-driver.js');
 
 /* eslint-env jest */
@@ -130,6 +131,7 @@ describe('collectPhaseArtifacts', () => {
           phase,
           gatherMode: /** @type {any} */ (gatherMode),
           computedCache: new Map(),
+          settings: defaultSettings,
         });
         expect(artifactState[phase]).toEqual({
           Timespan: expect.any(Promise),
@@ -152,6 +154,7 @@ describe('collectPhaseArtifacts', () => {
       gatherMode: 'navigation',
       phase: 'getArtifact',
       computedCache: new Map(),
+      settings: defaultSettings,
     });
     expect(await artifactState.getArtifact.Snapshot).toEqual({type: 'snapshot'});
     expect(await artifactState.getArtifact.Timespan).toEqual({type: 'timespan'});
@@ -173,6 +176,7 @@ describe('collectPhaseArtifacts', () => {
       gatherMode: 'navigation',
       phase: 'getArtifact',
       computedCache: new Map(),
+      settings: defaultSettings,
     });
     expect(artifactState.getArtifact).toEqual({
       Dependency: expect.any(Promise),
@@ -204,6 +208,7 @@ describe('collectPhaseArtifacts', () => {
       gatherMode: 'navigation',
       phase: 'getArtifact',
       computedCache: new Map(),
+      settings: defaultSettings,
     });
     expect(artifactState.getArtifact).toEqual({
       Snapshot: expect.any(Promise),
diff --git a/lighthouse-core/test/gather/gatherers/css-usage-test.js b/lighthouse-core/test/gather/gatherers/css-usage-test.js
index bc2a4f40e..f3a7a54e9 100644
--- a/lighthouse-core/test/gather/gatherers/css-usage-test.js
+++ b/lighthouse-core/test/gather/gatherers/css-usage-test.js
@@ -8,6 +8,7 @@
 /* eslint-env jest */
 
 const CSSUsage = require('../../../gather/gatherers/css-usage.js');
+const {defaultSettings} = require('../../../config/constants.js');
 const {createMockDriver} = require('../../fraggle-rock/gather/mock-driver.js');
 
 describe('.getArtifact', () => {
@@ -38,6 +39,7 @@ describe('.getArtifact', () => {
       gatherMode: 'snapshot',
       computedCache: new Map(),
       dependencies: {},
+      settings: defaultSettings,
     };
     const gatherer = new CSSUsage();
     const artifact = await gatherer.getArtifact(context);
@@ -92,6 +94,7 @@ describe('.getArtifact', () => {
       gatherMode: 'snapshot',
       computedCache: new Map(),
       dependencies: {},
+      settings: defaultSettings,
     };
     const gatherer = new CSSUsage();
     const artifact = await gatherer.getArtifact(context);
diff --git a/lighthouse-core/test/gather/gatherers/full-page-screenshot-test.js b/lighthouse-core/test/gather/gatherers/full-page-screenshot-test.js
index d5eda847c..ced41c7d1 100644
--- a/lighthouse-core/test/gather/gatherers/full-page-screenshot-test.js
+++ b/lighthouse-core/test/gather/gatherers/full-page-screenshot-test.js
@@ -7,19 +7,30 @@
 
 /* eslint-env jest */
 
-let mockEmulate = jest.fn();
-jest.mock('../../../lib/emulation.js', () => ({emulate: (...args) => mockEmulate(...args)}));
-
+const {
+  createMockContext,
+  mockDriverSubmodules,
+} = require('../../fraggle-rock/gather/mock-driver.js');
+const mocks = mockDriverSubmodules();
 const FullPageScreenshotGatherer = require('../../../gather/gatherers/full-page-screenshot.js');
 
 // Headless's default value is (1024 * 16), but this varies by device
 const maxTextureSizeMock = 1024 * 8;
 
-/**
- * @param {{contentSize: {width: number, height: number}, screenSize: {width?: number, height?: number, dpr: number}, screenshotData: string[]}}
- */
-function createMockDriver({contentSize, screenSize, screenshotData}) {
-  const sendCommand = jest.fn().mockImplementation(method => {
+/** @type {{width: number, height: number}} */
+let contentSize;
+/** @type {{width?: number, height?: number, dpr: number}} */
+let screenSize;
+/** @type {string[]} */
+let screenshotData;
+let mockContext = createMockContext();
+
+beforeEach(() => {
+  contentSize = {width: 100, height: 100};
+  screenSize = {dpr: 1};
+  screenshotData = [];
+  mockContext = createMockContext();
+  mockContext.driver.defaultSession.sendCommand.mockImplementation(method => {
     if (method === 'Page.getLayoutMetrics') {
       return {
         contentSize,
@@ -33,62 +44,44 @@ function createMockDriver({contentSize, screenSize, screenshotData}) {
       };
     }
   });
-
-  return {
-    executionContext: {
-      evaluate: async function(fn) {
-        if (fn.name === 'resolveNodes') {
-          return {};
-        } if (fn.name === 'getMaxTextureSize') {
-          return maxTextureSizeMock;
-        } else if (fn.name === 'getObservedDeviceMetrics') {
-          return {
-            width: screenSize.width,
-            height: screenSize.height,
-            screenWidth: screenSize.width,
-            screenHeight: screenSize.height,
-            screenOrientation: {
-              type: 'landscapePrimary',
-              angle: 30,
-            },
-            deviceScaleFactor: screenSize.dpr,
-          };
-        } else {
-          throw new Error(`unexpected fn ${fn.name}`);
-        }
-      },
-    },
-    sendCommand,
-    defaultSession: {sendCommand},
-  };
-}
-
-describe('FullPageScreenshot gatherer', () => {
-  beforeEach(() => {
-    mockEmulate = jest.fn();
+  mockContext.driver._executionContext.evaluate.mockImplementation(fn => {
+    if (fn.name === 'resolveNodes') {
+      return {};
+    } if (fn.name === 'getMaxTextureSize') {
+      return maxTextureSizeMock;
+    } else if (fn.name === 'getObservedDeviceMetrics') {
+      return {
+        width: screenSize.width,
+        height: screenSize.height,
+        screenWidth: screenSize.width,
+        screenHeight: screenSize.height,
+        screenOrientation: {
+          type: 'landscapePrimary',
+          angle: 30,
+        },
+        deviceScaleFactor: screenSize.dpr,
+      };
+    } else {
+      throw new Error(`unexpected fn ${fn.name}`);
+    }
   });
+  mocks.reset();
+});
 
+describe('FullPageScreenshot gatherer', () => {
   it('captures a full-page screenshot', async () => {
     const fpsGatherer = new FullPageScreenshotGatherer();
-    const driver = createMockDriver({
-      contentSize: {
-        width: 412,
-        height: 2000,
-      },
-    });
-    const passContext = {
-      settings: {
-        formFactor: 'mobile',
-        screenEmulation: {
-          mobile: true,
-          disabled: false,
-        },
+    contentSize = {width: 412, height: 2000};
+
+    mockContext.settings = {
+      formFactor: 'mobile',
+      screenEmulation: {
+        mobile: true,
+        disabled: false,
       },
-      driver,
-      baseArtifacts: {},
     };
+    const artifact = await fpsGatherer.getArtifact(mockContext.asContext());
 
-    const artifact = await fpsGatherer.afterPass(passContext);
     expect(artifact).toEqual({
       screenshot: {
         data: 'data:image/jpeg;base64,abc',
@@ -101,59 +94,41 @@ describe('FullPageScreenshot gatherer', () => {
 
   it('resets the emulation correctly when Lighthouse controls it', async () => {
     const fpsGatherer = new FullPageScreenshotGatherer();
-    const driver = createMockDriver({
-      contentSize: {
-        width: 412,
-        height: 2000,
-      },
-    });
-    const passContext = {
-      settings: {
-        formFactor: 'mobile',
-        screenEmulation: {
-          mobile: true,
-          disabled: false,
-        },
+    contentSize = {width: 412, height: 2000};
+    mockContext.settings = {
+      formFactor: 'mobile',
+      screenEmulation: {
+        mobile: true,
+        disabled: false,
       },
-      driver,
-      baseArtifacts: {},
     };
 
-    await fpsGatherer.afterPass(passContext);
+    await fpsGatherer.getArtifact(mockContext.asContext());
 
     const expectedArgs = {formFactor: 'mobile', screenEmulation: {disabled: false, mobile: true}};
-    expect(mockEmulate).toHaveBeenCalledTimes(1);
-    expect(mockEmulate).toHaveBeenCalledWith(driver.defaultSession, expectedArgs);
+    expect(mocks.emulationMock.emulate).toHaveBeenCalledTimes(1);
+    expect(mocks.emulationMock.emulate).toHaveBeenCalledWith(
+      mockContext.driver.defaultSession,
+      expectedArgs
+    );
   });
 
   it('resets the emulation correctly when Lighthouse does not control it', async () => {
     const fpsGatherer = new FullPageScreenshotGatherer();
-    const driver = createMockDriver({
-      contentSize: {
-        width: 500,
-        height: 1500,
-      },
-      screenSize: {
-        width: 500,
-        height: 500,
-        dpr: 2,
-      },
-    });
-    const passContext = {
-      settings: {
-        screenEmulation: {
-          mobile: true,
-          disabled: true,
-        },
-        formFactor: 'mobile',
+    contentSize = {width: 500, height: 1500};
+    screenSize = {width: 500, height: 500, dpr: 2};
+    mockContext.settings = {
+      screenEmulation: {
+        mobile: true,
+        disabled: true,
       },
-      driver,
+      formFactor: 'mobile',
     };
 
-    await fpsGatherer.afterPass(passContext);
+    await fpsGatherer.getArtifact(mockContext.asContext());
 
     // Setting up for screenshot.
-    expect(driver.sendCommand).toHaveBeenCalledWith(
+    expect(mockContext.driver.defaultSession.sendCommand).toHaveBeenCalledWith(
       'Emulation.setDeviceMetricsOverride',
       expect.objectContaining({
         mobile: true,
@@ -164,7 +139,7 @@ describe('FullPageScreenshot gatherer', () => {
     );
 
     // Restoring.
-    expect(driver.sendCommand).toHaveBeenCalledWith(
+    expect(mockContext.driver.defaultSession.sendCommand).toHaveBeenCalledWith(
       'Emulation.setDeviceMetricsOverride',
       expect.objectContaining({
         mobile: true,
@@ -181,30 +156,20 @@ describe('FullPageScreenshot gatherer', () => {
 
   it('limits the screenshot height to the max Chrome can capture', async () => {
     const fpsGatherer = new FullPageScreenshotGatherer();
-    const driver = createMockDriver({
-      contentSize: {
-        width: 412,
-        height: 100000,
-      },
-      screenSize: {
-        dpr: 1,
-      },
-    });
-    const passContext = {
-      settings: {
-        formFactor: 'mobile',
-        screenEmulation: {
-          mobile: true,
-          disabled: false,
-        },
+
+    contentSize = {width: 412, height: 100000};
+    screenSize = {dpr: 1};
+    mockContext.settings = {
+      formFactor: 'mobile',
+      screenEmulation: {
+        mobile: true,
+        disabled: false,
       },
-      driver,
-      baseArtifacts: {},
     };
 
-    await fpsGatherer.afterPass(passContext);
+    await fpsGatherer.getArtifact(mockContext.asContext());
 
-    expect(driver.sendCommand).toHaveBeenCalledWith(
+    expect(mockContext.driver.defaultSession.sendCommand).toHaveBeenCalledWith(
       'Emulation.setDeviceMetricsOverride',
       expect.objectContaining({
         deviceScaleFactor: 1,
diff --git a/lighthouse-core/test/report/report-generator-test.js b/lighthouse-core/test/report/report-generator-test.js
index 90e4e7157..98cdfc21d 100644
--- a/lighthouse-core/test/report/report-generator-test.js
+++ b/lighthouse-core/test/report/report-generator-test.js
@@ -104,7 +104,7 @@ describe('ReportGenerator', () => {
       expect(lines.length).toBeGreaterThan(100);
       expect(lines.slice(0, 3).join('\n')).toMatchInlineSnapshot(`
         "requestedUrl,finalUrl,category,name,title,type,score
-        \\"http://localhost:10200/dobetterweb/dbw_tester.html\\",\\"http://localhost:10200/dobetterweb/dbw_tester.html\\",\\"Performance\\",\\"performance-score\\",\\"Overall Performance Category Score\\",\\"numeric\\",\\"0.6\\"
+        \\"http://localhost:10200/dobetterweb/dbw_tester.html\\",\\"http://localhost:10200/dobetterweb/dbw_tester.html\\",\\"Performance\\",\\"performance-score\\",\\"Overall Performance Category Score\\",\\"numeric\\",\\"0.59\\"
         \\"http://localhost:10200/dobetterweb/dbw_tester.html\\",\\"http://localhost:10200/dobetterweb/dbw_tester.html\\",\\"Performance\\",\\"first-contentful-paint\\",\\"First Contentful Paint\\",\\"numeric\\",\\"0.24\\"
         "
       `);
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index cbac6f1ed..83e053ef1 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -185,7 +185,7 @@
       "id": "total-blocking-time",
       "title": "Total Blocking Time",
       "description": "Sum of all time periods between FCP and Time to Interactive, when task length exceeded 50ms, expressed in milliseconds. [Learn more](https://web.dev/lighthouse-total-blocking-time/).",
-      "score": 1,
+      "score": 0.97,
       "scoreDisplayMode": "numeric",
       "numericValue": 116.79800000000023,
       "numericUnit": "millisecond",
@@ -5276,7 +5276,7 @@
         }
       ],
       "id": "performance",
-      "score": 0.6
+      "score": 0.59
     },
     "accessibility": {
       "title": "Accessibility",
diff --git a/third-party/chromium-webtests/webtests/http/tests/devtools/lighthouse/lighthouse-flags-run.js b/third-party/chromium-webtests/webtests/http/tests/devtools/lighthouse/lighthouse-flags-run.js
index 1630e76c3..e5a93433b 100644
--- a/third-party/chromium-webtests/webtests/http/tests/devtools/lighthouse/lighthouse-flags-run.js
+++ b/third-party/chromium-webtests/webtests/http/tests/devtools/lighthouse/lighthouse-flags-run.js
@@ -25,7 +25,7 @@
   TestRunner.addResult(`disableStorageReset: ${lhr.configSettings.disableStorageReset}`);
   TestRunner.addResult(`throttlingMethod: ${lhr.configSettings.throttlingMethod}`);
 
-  const viewTraceButton = LighthouseTestRunner.getResultsElement().querySelector('.view-trace');
+  const viewTraceButton = LighthouseTestRunner.getResultsElement().querySelector('.lh-button--trace');
   TestRunner.addResult(`\nView Trace Button Text: "${viewTraceButton.textContent}"`);
   TestRunner.addResult(`View Trace Button Title: "${viewTraceButton.title}"`);
 
diff --git a/third-party/chromium-webtests/webtests/http/tests/devtools/lighthouse/lighthouse-view-trace-run.js b/third-party/chromium-webtests/webtests/http/tests/devtools/lighthouse/lighthouse-view-trace-run.js
index 158378c7e..5264bc578 100644
--- a/third-party/chromium-webtests/webtests/http/tests/devtools/lighthouse/lighthouse-view-trace-run.js
+++ b/third-party/chromium-webtests/webtests/http/tests/devtools/lighthouse/lighthouse-view-trace-run.js
@@ -31,7 +31,7 @@
   const waitForShowView = new Promise(resolve => {
     TestRunner.addSniffer(UI.ViewManager.prototype, 'showView', resolve);
   });
-  const viewTraceButton = LighthouseTestRunner.getResultsElement().querySelector('.view-trace');
+  const viewTraceButton = LighthouseTestRunner.getResultsElement().querySelector('.lh-button--trace');
   TestRunner.addResult(`\nView Trace Button Text: "${viewTraceButton.textContent}"`);
   TestRunner.addResult(`View Trace Button Title: "${UI.Tooltip.getContent(viewTraceButton)}"`);
   viewTraceButton.click();