Repository: GoogleChrome/lighthouse
Pull request #12913 (2021-08-13 17:29:57)

core(fr): add protocol timeout to session
**Summary**
Adds protocol timeout support to Fraggle Rock session.

**Related Issues/PRs**
ref #12861 #11313 



Patch:
diff --git a/build/build-bundle.js b/build/build-bundle.js
index c29375081..5156133c8 100644
--- a/build/build-bundle.js
+++ b/build/build-bundle.js
@@ -13,6 +13,7 @@
 const fs = require('fs');
 const path = require('path');
 const assert = require('assert').strict;
+const stream = require('stream');
 const mkdir = fs.promises.mkdir;
 const LighthouseRunner = require('../lighthouse-core/runner.js');
 const exorcist = require('exorcist');
@@ -66,7 +67,23 @@ async function browserifyFile(entryPath, distPath) {
     })
     // Transform the fs.readFile etc into inline strings.
     .transform('@wardpeet/brfs', {
-      readFileSyncTransform: minifyFileTransform,
+      /** @param {string} file */
+      readFileTransform: (file) => {
+        // Don't include locales in DevTools.
+        if (isDevtools(entryPath) && locales.includes(file)) {
+          return new stream.Transform({
+            transform(chunk, enc, next) {
+              next();
+            },
+            final(next) {
+              this.push('{}');
+              next();
+            },
+          });
+        }
+
+        return minifyFileTransform(file);
+      },
       global: true,
       parserOpts: {ecmaVersion: 12},
     })
@@ -90,12 +107,6 @@ async function browserifyFile(entryPath, distPath) {
     bundle.ignore(require.resolve('../report/report-assets.js'));
   }
 
-  // Don't include locales in DevTools.
-  if (isDevtools(entryPath)) {
-    // @ts-expect-error bundle.ignore does accept an array of strings.
-    bundle.ignore(locales);
-  }
-
   // Expose the audits, gatherers, and computed artifacts so they can be dynamically loaded.
   // Exposed path must be a relative path from lighthouse-core/config/config-helpers.js (where loading occurs).
   const corePath = './lighthouse-core/';
diff --git a/build/build-dt-report-resources.js b/build/build-dt-report-resources.js
index 13a74467f..f18700a37 100644
--- a/build/build-dt-report-resources.js
+++ b/build/build-dt-report-resources.js
@@ -33,7 +33,6 @@ fs.mkdirSync(distDir, {recursive: true});
 writeFile('report.js', htmlReportAssets.REPORT_JAVASCRIPT);
 writeFile('report.css', htmlReportAssets.REPORT_CSS);
 writeFile('standalone-template.html', htmlReportAssets.REPORT_TEMPLATE);
-writeFile('templates.html', '<div>Empty file. Remove on next roll to CDT</div>');
 writeFile('report.d.ts', 'export {}');
 writeFile('report-generator.d.ts', 'export {}');
 
diff --git a/build/build-lightrider-bundles.js b/build/build-lightrider-bundles.js
index 27970be71..6a570d397 100644
--- a/build/build-lightrider-bundles.js
+++ b/build/build-lightrider-bundles.js
@@ -40,7 +40,7 @@ function buildReportGenerator() {
   browserify(generatorFilename, {standalone: 'ReportGenerator'})
     // Transform the fs.readFile etc into inline strings.
     .transform('@wardpeet/brfs', {
-      readFileSyncTransform: minifyFileTransform,
+      readFileTransform: minifyFileTransform,
       global: true,
       parserOpts: {ecmaVersion: 12},
     })
diff --git a/build/build-sample-reports.js b/build/build-sample-reports.js
index 963639505..ebe539e38 100644
--- a/build/build-sample-reports.js
+++ b/build/build-sample-reports.js
@@ -19,7 +19,7 @@ const {LH_ROOT} = require('../root.js');
 const htmlReportAssets = require('../lighthouse-core/../report/report-assets.js');
 
 
-const DIST = path.join(LH_ROOT, `dist/now`);
+const DIST = path.join(LH_ROOT, 'dist');
 
 (async function() {
   addPluginCategory(lhr);
@@ -47,8 +47,7 @@ const DIST = path.join(LH_ROOT, `dist/now`);
         // - a more constrained/realistic default size
         html = html.replace(`"lh-root lh-vars"`, `"lh-root lh-vars lh-devtools"`);
       }
-
-      const filepath = `${DIST}/${variant}${filename}/index.html`;
+      const filepath = `${DIST}/sample-reports/${variant}${filename}/index.html`;
       fs.mkdirSync(path.dirname(filepath), {recursive: true});
       fs.writeFileSync(filepath, html, {encoding: 'utf-8'});
       console.log('✅', filepath, 'written.');
diff --git a/build/build-viewer.js b/build/build-viewer.js
index 32afb9f24..2794a458e 100644
--- a/build/build-viewer.js
+++ b/build/build-viewer.js
@@ -20,7 +20,7 @@ async function run() {
   const generatorFilename = `${LH_ROOT}/report/report-generator.js`;
   const generatorBrowserify = browserify(generatorFilename, {standalone: 'ReportGenerator'})
     .transform('@wardpeet/brfs', {
-      readFileSyncTransform: minifyFileTransform,
+      readFileTransform: minifyFileTransform,
     });
 
   /** @type {Promise<string>} */
diff --git a/docs/hacking-tips.md b/docs/hacking-tips.md
index 25140c015..74735a069 100644
--- a/docs/hacking-tips.md
+++ b/docs/hacking-tips.md
@@ -1,5 +1,10 @@
 A few assorted scripts and tips to make hacking on Lighthouse a bit easier
 
+# Eng team resources
+
+* [LH Build Tracker](https://lh-build-tracker.herokuapp.com/builds/limit/100) - plotted results of [build-tracker](../build-tracker.config.js) [data](../.github/workflows/ci.yml#:~:text=buildtracker)
+* [LH PR Tracking](https://paulirish.github.io/lh-pr-tracking/) - stats about open PRs, collected [daily](https://github.com/paulirish/lh-pr-tracking/blob/master/.github/workflows/update-stats.yml).
+
 ## Evaluate Lighthouse's runtime performance
 
 Lighthouse has instrumentation to collect timing data for its operations. The data is exposed at `LHR.timing.entries`.  You can generate a trace from this data for closer analysis.
diff --git a/docs/recipes/auth/README.md b/docs/recipes/auth/README.md
index b2aa3db76..0183b7582 100644
--- a/docs/recipes/auth/README.md
+++ b/docs/recipes/auth/README.md
@@ -21,6 +21,7 @@ The server responds with different HTML for each of these pages and session stat
 ```sh
 # be in root lighthouse directory
 yarn # install global project deps
+yarn build-report
 cd docs/recipes/auth
 yarn # install deps related to just this recipe
 yarn start # start the server on http://localhost:10632
diff --git a/lighthouse-core/config/config.js b/lighthouse-core/config/config.js
index 43b608ac8..59d2014a9 100644
--- a/lighthouse-core/config/config.js
+++ b/lighthouse-core/config/config.js
@@ -508,12 +508,10 @@ class Config {
 
     // The `full-page-screenshot` audit belongs to no category, but we still want to include
     // it (unless explictly excluded) because there are audits in every category that can use it.
-    if (settings.onlyCategories) {
-      const explicitlyExcludesFullPageScreenshot =
-        settings.skipAudits && settings.skipAudits.includes('full-page-screenshot');
-      if (!explicitlyExcludesFullPageScreenshot) {
-        includedAudits.add('full-page-screenshot');
-      }
+    const explicitlyExcludesFullPageScreenshot =
+      settings.skipAudits && settings.skipAudits.includes('full-page-screenshot');
+    if (!explicitlyExcludesFullPageScreenshot && (settings.onlyCategories || settings.skipAudits)) {
+      includedAudits.add('full-page-screenshot');
     }
 
     return {categories, requestedAuditNames: includedAudits};
diff --git a/lighthouse-core/fraggle-rock/gather/session.js b/lighthouse-core/fraggle-rock/gather/session.js
index b5d34b044..4168ccd96 100644
--- a/lighthouse-core/fraggle-rock/gather/session.js
+++ b/lighthouse-core/fraggle-rock/gather/session.js
@@ -5,8 +5,12 @@
  */
 'use strict';
 
+const LHError = require('../../lib/lh-error.js');
 const SessionEmitMonkeypatch = Symbol('monkeypatch');
 
+// Controls how long to wait for a response after sending a DevTools protocol command.
+const DEFAULT_PROTOCOL_TIMEOUT = 30000;
+
 /** @implements {LH.Gatherer.FRProtocolSession} */
 class ProtocolSession {
   /**
@@ -14,6 +18,8 @@ class ProtocolSession {
    */
   constructor(session) {
     this._session = session;
+    /** @type {number|undefined} */
+    this._nextProtocolTimeout = undefined;
 
     // FIXME: Monkeypatch puppeteer to be able to listen to *all* protocol events.
     // This patched method will now emit a copy of every event on `*`.
@@ -32,21 +38,21 @@ class ProtocolSession {
    * @return {boolean}
    */
   hasNextProtocolTimeout() {
-    return false;
+    return this._nextProtocolTimeout !== undefined;
   }
 
   /**
    * @return {number}
    */
   getNextProtocolTimeout() {
-    return Number.MAX_SAFE_INTEGER;
+    return this._nextProtocolTimeout || DEFAULT_PROTOCOL_TIMEOUT;
   }
 
   /**
    * @param {number} ms
    */
-  setNextProtocolTimeout(ms) { // eslint-disable-line no-unused-vars
-    // TODO(FR-COMPAT): support protocol timeout
+  setNextProtocolTimeout(ms) {
+    this._nextProtocolTimeout = ms;
   }
 
   /**
@@ -102,7 +108,26 @@ class ProtocolSession {
    * @return {Promise<LH.CrdpCommands[C]['returnType']>}
    */
   sendCommand(method, ...params) {
-    return this._session.send(method, ...params);
+    const timeoutMs = this.getNextProtocolTimeout();
+    this._nextProtocolTimeout = undefined;
+
+    /** @type {NodeJS.Timer|undefined} */
+    let timeout;
+    const timeoutPromise = new Promise((resolve, reject) => {
+      if (timeoutMs === Infinity) return;
+
+      timeout = setTimeout((() => {
+        const err = new LHError(LHError.errors.PROTOCOL_TIMEOUT, {protocolMethod: method});
+        reject(err);
+      }), timeoutMs);
+    });
+
+    const resultPromise = this._session.send(method, ...params);
+    const resultWithTimeoutPromise = Promise.race([resultPromise, timeoutPromise]);
+
+    return resultWithTimeoutPromise.finally(() => {
+      if (timeout) clearTimeout(timeout);
+    });
   }
 }
 
diff --git a/lighthouse-core/lib/asset-saver.js b/lighthouse-core/lib/asset-saver.js
index d4aaff2f9..2d5f576b3 100644
--- a/lighthouse-core/lib/asset-saver.js
+++ b/lighthouse-core/lib/asset-saver.js
@@ -142,7 +142,7 @@ function saveLhr(lhr, basePath) {
 /**
  * Filter traces and extract screenshots to prepare for saving.
  * @param {LH.Artifacts} artifacts
- * @param {LH.Audit.Results} [audits]
+ * @param {LH.Result['audits']} [audits]
  * @return {Promise<Array<PreparedAssets>>}
  */
 async function prepareAssets(artifacts, audits) {
@@ -272,7 +272,7 @@ async function saveLanternDebugTraces(pathWithBasename) {
 /**
  * Writes trace(s) and associated asset(s) to disk.
  * @param {LH.Artifacts} artifacts
- * @param {LH.Audit.Results} audits
+ * @param {LH.Result['audits']} audits
  * @param {string} pathWithBasename
  * @return {Promise<void>}
  */
diff --git a/lighthouse-core/lib/i18n/i18n.js b/lighthouse-core/lib/i18n/i18n.js
index 977d21a09..2c054e2e7 100644
--- a/lighthouse-core/lib/i18n/i18n.js
+++ b/lighthouse-core/lib/i18n/i18n.js
@@ -340,19 +340,19 @@ function _formatPathAsString(pathInLHR) {
 
 /**
  * @param {LH.Locale} locale
- * @return {LH.I18NRendererStrings}
+ * @return {Record<string, string>}
  */
 function getRendererFormattedStrings(locale) {
   const localeMessages = LOCALES[locale];
   if (!localeMessages) throw new Error(`Unsupported locale '${locale}'`);
 
   const icuMessageIds = Object.keys(localeMessages).filter(f => f.startsWith('report/'));
-  const strings = /** @type {LH.I18NRendererStrings} */ ({});
+  /** @type {Record<string, string>} */
+  const strings = {};
   for (const icuMessageId of icuMessageIds) {
-    const [filename, varName] = icuMessageId.split(' | ');
+    const [filename, key] = icuMessageId.split(' | ');
     if (!filename.endsWith('util.js')) throw new Error(`Unexpected message: ${icuMessageId}`);
 
-    const key = /** @type {keyof LH.I18NRendererStrings} */ (varName);
     strings[key] = localeMessages[icuMessageId].message;
   }
 
@@ -463,12 +463,12 @@ function getFormatted(icuMessageOrRawString, locale) {
  * that location.
  * @param {unknown} inputObject
  * @param {LH.Locale} locale
- * @return {LH.IcuMessagePaths}
+ * @return {LH.Result.IcuMessagePaths}
  */
 function replaceIcuMessages(inputObject, locale) {
   /**
    * @param {unknown} subObject
-   * @param {LH.IcuMessagePaths} icuMessagePaths
+   * @param {LH.Result.IcuMessagePaths} icuMessagePaths
    * @param {string[]} pathInLHR
    */
   function replaceInObject(subObject, icuMessagePaths, pathInLHR = []) {
@@ -498,7 +498,7 @@ function replaceIcuMessages(inputObject, locale) {
     }
   }
 
-  /** @type {LH.IcuMessagePaths} */
+  /** @type {LH.Result.IcuMessagePaths} */
   const icuMessagePaths = {};
   replaceInObject(inputObject, icuMessagePaths);
   return icuMessagePaths;
diff --git a/lighthouse-core/lib/traces/pwmetrics-events.js b/lighthouse-core/lib/traces/pwmetrics-events.js
index 867e9061d..f1ccb7543 100644
--- a/lighthouse-core/lib/traces/pwmetrics-events.js
+++ b/lighthouse-core/lib/traces/pwmetrics-events.js
@@ -9,7 +9,7 @@ const log = require('lighthouse-logger');
 const TraceProcessor = require('../tracehouse/trace-processor.js');
 
 /**
- * @param {LH.Audit.Results} auditResults
+ * @param {LH.Result['audits']} auditResults
  * @return {LH.Artifacts.TimingSummary|undefined}
  */
 function getUberMetrics(auditResults) {
@@ -22,7 +22,7 @@ function getUberMetrics(auditResults) {
 class Metrics {
   /**
    * @param {Array<LH.TraceEvent>} traceEvents
-   * @param {LH.Audit.Results} auditResults
+   * @param {LH.Result['audits']} auditResults
    */
   constructor(traceEvents, auditResults) {
     this._traceEvents = traceEvents;
diff --git a/lighthouse-core/scripts/dogfood-lhci.sh b/lighthouse-core/scripts/dogfood-lhci.sh
index f3b3eb4bf..b94525d16 100755
--- a/lighthouse-core/scripts/dogfood-lhci.sh
+++ b/lighthouse-core/scripts/dogfood-lhci.sh
@@ -35,14 +35,13 @@ if ! echo "$CHANGED_FILES" | grep -E 'report|lhci' > /dev/null; then
   exit 0
 fi
 
-# Generate HTML reports in ./dist/now/
-yarn build-report
+# Generate HTML reports in ./dist/sample-reports
 yarn vercel-build
 
 # Install LHCI
 npm install -g @lhci/cli@next
 # Collect our LHCI results.
-lhci collect --staticDistDir=./dist/now/english/
+lhci collect --staticDistDir=./dist/sample-reports/english
 # Upload the results to our canary server.
 lhci upload \
   --serverBaseUrl="$LHCI_CANARY_SERVER_URL" \
diff --git a/lighthouse-treemap/tsconfig.json b/lighthouse-treemap/tsconfig.json
index 05257da2d..0d715b755 100644
--- a/lighthouse-treemap/tsconfig.json
+++ b/lighthouse-treemap/tsconfig.json
@@ -7,5 +7,6 @@
   ],
   "compilerOptions": {
     "esModuleInterop": true,
+    "composite": false,
   },
 }
diff --git a/lighthouse-viewer/tsconfig.json b/lighthouse-viewer/tsconfig.json
index 72c40535f..3eb83b0c3 100644
--- a/lighthouse-viewer/tsconfig.json
+++ b/lighthouse-viewer/tsconfig.json
@@ -1,16 +1,12 @@
 {
   "extends": "../tsconfig",
   "compilerOptions": {
-    "typeRoots": [
-      "../node_modules/@types",
-      "../types",
-      "./types",
-    ],
     "esModuleInterop": true,
+    "composite": false,
   },
   "include": [
     "app/src/**/*.js",
-    "../types/*.d.ts",
+    "../types/**/*.d.ts",
     "./types/*.d.ts",
   ],
 }
diff --git a/now.json b/now.json
index ac2df9641..adea964ae 100644
--- a/now.json
+++ b/now.json
@@ -4,7 +4,7 @@
     {
       "src": "package.json",
       "use": "@now/static-build",
-      "config": {"distDir": "dist/now"}
+      "config": {"distDir": "dist"}
     }
   ],
   "github": {
diff --git a/package.json b/package.json
index 0aac58bab..e0c077c99 100644
--- a/package.json
+++ b/package.json
@@ -63,11 +63,11 @@
     "fast": "node ./lighthouse-cli/index.js --preset=desktop --throttlingMethod=provided",
     "deploy-treemap": "yarn build-treemap --deploy",
     "deploy-viewer": "yarn build-viewer --deploy",
-    "vercel-build": "yarn build-sample-reports && yarn build-viewer && yarn build-treemap && cp -r dist/gh-pages dist/now/gh-pages",
+    "vercel-build": "yarn build-sample-reports && yarn build-viewer && yarn build-treemap",
     "dogfood-lhci": "./lighthouse-core/scripts/dogfood-lhci.sh",
     "timing-trace": "node lighthouse-core/scripts/generate-timing-trace.js",
     "changelog": "conventional-changelog --config ./build/changelog-generator/index.js --infile changelog.md --same-file",
-    "type-check": "tsc -p . && tsc -p lighthouse-viewer/ && tsc -p lighthouse-treemap/",
+    "type-check": "tsc -b . && tsc -p lighthouse-viewer/ && tsc -p lighthouse-treemap/",
     "i18n:checks": "./lighthouse-core/scripts/i18n/assert-strings-collected.sh",
     "i18n:collect-strings": "node lighthouse-core/scripts/i18n/collect-strings.js",
     "update:lantern-baseline": "node lighthouse-core/scripts/lantern/update-baseline-lantern-values.js",
diff --git a/report/renderer/element-screenshot-renderer.js b/report/renderer/element-screenshot-renderer.js
index 93fc829b1..1de2acf5a 100644
--- a/report/renderer/element-screenshot-renderer.js
+++ b/report/renderer/element-screenshot-renderer.js
@@ -27,7 +27,7 @@ import {Util} from './util.js';
 
 /**
  * @param {LH.Artifacts.FullPageScreenshot['screenshot']} screenshot
- * @param {LH.Artifacts.Rect} rect
+ * @param {LH.Audit.Details.Rect} rect
  * @return {boolean}
  */
 function screenshotOverlapsRect(screenshot, rect) {
diff --git a/report/renderer/report-renderer.js b/report/renderer/report-renderer.js
index 8e9cedacb..c91133efd 100644
--- a/report/renderer/report-renderer.js
+++ b/report/renderer/report-renderer.js
@@ -54,13 +54,6 @@ export class ReportRenderer {
     return container;
   }
 
-  /**
-   * @param {ParentNode} _
-   */
-  setTemplateContext(_) {
-    // Removed, exists until usage is removed from CDT.
-  }
-
   /**
    * @param {LH.ReportResult} report
    * @return {DocumentFragment}
diff --git a/report/renderer/report-ui-features.js b/report/renderer/report-ui-features.js
index 10003d966..f93fb0897 100644
--- a/report/renderer/report-ui-features.js
+++ b/report/renderer/report-ui-features.js
@@ -166,18 +166,12 @@ export class ReportUIFeatures {
     for (const node of this._dom.findAll('[data-i18n]', this._dom.document())) {
       // These strings are guaranteed to (at least) have a default English string in Util.UIStrings,
       // so this cannot be undefined as long as `report-ui-features.data-i18n` test passes.
-      const i18nAttr = /** @type {keyof LH.I18NRendererStrings} */ (node.getAttribute('data-i18n'));
+      const i18nKey = node.getAttribute('data-i18n');
+      const i18nAttr = /** @type {keyof typeof Util.i18n.strings} */ (i18nKey);
       node.textContent = Util.i18n.strings[i18nAttr];
     }
   }
 
-  /**
-   * @param {ParentNode} _
-   */
-  setTemplateContext(_) {
-    // Removed, exists until usage is removed from CDT.
-  }
-
   /**
    * @param {{container?: Element, text: string, icon?: string, onClick: () => void}} opts
    */
diff --git a/tsconfig.json b/tsconfig.json
index cf1c579d6..fff850e6c 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -12,9 +12,12 @@
     // "noErrorTruncation": true,
 
     "resolveJsonModule": true,
-    "diagnostics": true
+    "diagnostics": true,
+    "composite": true,
+    "outDir": ".tmp/tsbuildinfo/",
   },
   "include": [
+    "root.js",
     "lighthouse-cli/**/*.js",
     "lighthouse-core/**/*.js",
     "report/**/*.js",
@@ -22,9 +25,22 @@
     "build/**/*.js",
     "./types/**/*.d.ts",
     "eslint-local-rules.js",
+
+    // TODO(esmodules): JSON files included via resolveJsonModule. Removable on the switch to ES Modules.
+    "package.json",
+    "lighthouse-core/lib/i18n/locales/en-US.json",
+    "lighthouse-core/test/results/sample_v2.json",
+    "lighthouse-core/lib/sd-validation/assets/*.json",
+    "lighthouse-core/test/fixtures/unresolved-perflog.json",
+    "lighthouse-core/test/fixtures/traces/lcp-m78.devtools.log.json",
+    "third-party/snyk/snapshot.json",
+    "lighthouse-core/audits/byte-efficiency/polyfill-graph-data.json",
+    "lighthouse-core/lib/large-javascript-libraries/bundlephobia-database.json",
+  ],
+  "references": [
+    {"path": "./types/lhr/"}
   ],
   "exclude": [
-    "lighthouse-core/lib/cdt",
     "lighthouse-core/test/audits/**/*.js",
     "lighthouse-core/test/fixtures/**/*.js",
     "lighthouse-core/test/computed/**/*.js",
@@ -105,7 +121,9 @@
     "lighthouse-core/test/scripts/i18n/collect-strings-test.js",
   ],
   "files": [
-    // Opt-in to typechecking for some core tests.
+    // Opt-in to typechecking for some core tests and test-support files.
     "lighthouse-core/test/audits/script-treemap-data-test.js",
+    "lighthouse-cli/test/fixtures/static-server.js",
+    "lighthouse-core/test/fixtures/config-plugins/lighthouse-plugin-simple/plugin-simple.js",
   ],
 }
diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts
index 7c9d01dfd..a901c0177 100644
--- a/types/artifacts.d.ts
+++ b/types/artifacts.d.ts
@@ -14,9 +14,11 @@ import ArbitraryEqualityMap = require('../lighthouse-core/lib/arbitrary-equality
 
 type _TaskNode = import('../lighthouse-core/lib/tracehouse/main-thread-tasks.js').TaskNode;
 
+import AuditDetails from './lhr/audit-details'
 import Config from './config';
 import Gatherer from './gatherer';
-import {IcuMessage} from './i18n';
+import {IcuMessage} from './lhr/i18n';
+import LHResult from './lhr/lhr'
 import Protocol from './protocol';
 
 export interface Artifacts extends BaseArtifacts, GathererArtifacts {}
@@ -532,14 +534,7 @@ declare module Artifacts {
     };
   }
 
-  interface Rect {
-    width: number;
-    height: number;
-    top: number;
-    right: number;
-    bottom: number;
-    left: number;
-  }
+  type Rect = AuditDetails.Rect;
 
   interface TapTarget {
     node: NodeDetails;
@@ -598,15 +593,7 @@ declare module Artifacts {
     }[];
   }
 
-  interface MeasureEntry {
-    // From PerformanceEntry
-    readonly duration: number;
-    readonly entryType: string;
-    readonly name: string;
-    readonly startTime: number;
-    /** Whether timing entry was collected during artifact gathering. */
-    gather?: boolean;
-  }
+  type MeasureEntry = LHResult.MeasureEntry;
 
   interface MetricComputationDataInput {
     devtoolsLog: DevtoolsLog;
diff --git a/types/audit.d.ts b/types/audit.d.ts
index 1fc2f25b8..755d47a93 100644
--- a/types/audit.d.ts
+++ b/types/audit.d.ts
@@ -6,13 +6,17 @@
 
 import ArbitraryEqualityMap = require('../lighthouse-core/lib/arbitrary-equality-map.js');
 import {Artifacts} from './artifacts';
-import AuditDetails from './audit-details';
+import AuditDetails from './lhr/audit-details';
 import Config from './config';
 import Gatherer from './gatherer';
-import {FormattedIcu, IcuMessage} from './i18n';
+import {FormattedIcu, IcuMessage} from './lhr/i18n';
+import * as AuditResult from './lhr/audit-result';
 
 declare module Audit {
   export import Details = AuditDetails;
+  export type Result = AuditResult.Result;
+  export type ScoreDisplayMode = AuditResult.ScoreDisplayMode;
+  export type ScoreDisplayModes = AuditResult.ScoreDisplayModes;
 
   type Context = Immutable<{
     /** audit options */
@@ -31,23 +35,6 @@ declare module Audit {
     median: number;
   }
 
-  interface ScoreDisplayModes {
-    /** Scores of 0-1 (map to displayed scores of 0-100). */
-    NUMERIC: 'numeric';
-    /** Pass/fail audit (0 and 1 are only possible scores). */
-    BINARY: 'binary';
-    /** The audit exists only to tell you to review something yourself. Score is null and should be ignored. */
-    MANUAL: 'manual';
-    /** The audit is an FYI only, and can't be interpreted as pass/fail. Score is null and should be ignored. */
-    INFORMATIVE: 'informative';
-    /** The audit turned out to not apply to the page. Score is null and should be ignored. */
-    NOT_APPLICABLE: 'notApplicable';
-    /** There was an error while running the audit (check `errorMessage` for details). Score is null and should be ignored. */
-    ERROR: 'error';
-  }
-
-  type ScoreDisplayMode = Audit.ScoreDisplayModes[keyof Audit.ScoreDisplayModes];
-
   interface Meta {
     /** The string identifier of the audit, in kebab case. */
     id: string;
@@ -62,7 +49,7 @@ declare module Audit {
     /** A list of the members of LH.Artifacts that augment the audit, but aren't necessary. For internal use only with experimental-config. */
     __internalOptionalArtifacts?: Array<keyof Artifacts>;
     /** A string identifying how the score should be interpreted for display. */
-    scoreDisplayMode?: Audit.ScoreDisplayMode;
+    scoreDisplayMode?: AuditResult.ScoreDisplayMode;
     /** A list of gather modes that this audit is applicable to. */
     supportedModes?: Gatherer.GatherMode[],
   }
@@ -111,44 +98,6 @@ declare module Audit {
   /** Type returned by Audit.audit(). Only score is required.  */
   type Product = NonNumericProduct | NumericProduct;
 
-  /* Audit result returned in Lighthouse report. All audits offer a description and score of 0-1. */
-  interface Result {
-    displayValue?: string;
-    /** An explanation of why the audit failed on the test page. */
-    explanation?: string;
-    /** Error message from any exception thrown while running this audit. */
-    errorMessage?: string;
-    warnings?: string[];
-    /** The scored value of the audit, provided in the range `0-1`, or null if `scoreDisplayMode` indicates not scored. */
-    score: number|null;
-    /**
-     * A string identifying how the score should be interpreted:
-     * - 'binary': pass/fail audit (0 and 1 are only possible scores).
-     * - 'numeric': scores of 0-1 (map to displayed scores of 0-100).
-     * - 'informative': the audit is an FYI only, and can't be interpreted as pass/fail. Score is null and should be ignored.
-     * - 'notApplicable': the audit turned out to not apply to the page. Score is null and should be ignored.
-     * - 'manual': The audit exists only to tell you to review something yourself. Score is null and should be ignored.
-     * - 'error': There was an error while running the audit (check `errorMessage` for details). Score is null and should be ignored.
-     */
-    scoreDisplayMode: ScoreDisplayMode;
-    /** Short, user-visible title for the audit. The text can change depending on if the audit passed or failed. */
-    title: string;
-    /** The string identifier of the audit, in kebab case. */
-    id: string;
-    /** A more detailed description that describes why the audit is important and links to Lighthouse documentation on the audit; markdown links supported. */
-    description: string;
-    /** A numeric value that has a meaning specific to the audit, e.g. the number of nodes in the DOM or the timestamp of a specific load event. More information can be found in the audit details, if present. */
-    numericValue?: number;
-    /** The unit of `numericValue`, used when the consumer wishes to convert numericValue to a display string. */
-    numericUnit?: string;
-    /** Extra information about the page provided by some types of audits, in one of several possible forms that can be rendered in the HTML report. */
-    details?: FormattedIcu<AuditDetails>;
-  }
-
-  interface Results {
-    [metric: string]: Result;
-  }
-
   type MultiCheckAuditP1 = Partial<Record<Artifacts.ManifestValueCheckID, boolean>>;
   type MultiCheckAuditP2 = Partial<Artifacts.ManifestValues>;
   interface MultiCheckAuditP3 {
diff --git a/types/config.d.ts b/types/config.d.ts
index a44122569..9a6fd22dc 100644
--- a/types/config.d.ts
+++ b/types/config.d.ts
@@ -6,9 +6,9 @@
  */
 
 import Audit = require('../lighthouse-core/audits/audit.js');
-import {ScreenEmulationSettings, SharedFlagsSettings, ThrottlingSettings} from './externs';
+import {SharedFlagsSettings, ConfigSettings} from './lhr/settings';
 import Gatherer from './gatherer';
-import {IcuMessage} from './i18n';
+import {IcuMessage} from './lhr/i18n';
 
 interface ClassOf<T> {
   new (): T;
@@ -161,10 +161,7 @@ declare module Config {
     relevantAudits?: string[];
   }
 
-  interface Settings extends Required<SharedFlagsSettings> {
-    throttling: Required<ThrottlingSettings>;
-    screenEmulation: ScreenEmulationSettings;
-  }
+  type Settings = ConfigSettings;
 
   interface Pass extends Required<PassJson> {
     gatherers: GathererDefn[];
diff --git a/types/externs.d.ts b/types/externs.d.ts
index cb25b91da..281d2b957 100644
--- a/types/externs.d.ts
+++ b/types/externs.d.ts
@@ -5,12 +5,10 @@
  */
 
 import {ParseSelectorToTagNames} from 'typed-query-selector/parser';
-import Crdp from 'devtools-protocol/types/protocol';
 
 import {Artifacts} from './artifacts';
-import Budget from './budget';
-import {Locale} from './i18n';
-import LHResult from './lhr';
+import LHResult from './lhr/lhr';
+import {SharedFlagsSettings, OutputMode} from './lhr/settings';
 
 /** Merge properties of the types in union `T`. Where properties overlap, property types becomes the union of the two (or more) possible types. */
 type MergeTypes<T> = {
@@ -148,97 +146,6 @@ declare global {
   }
 }
 
-/** Simulation settings that control the amount of network & cpu throttling in the run. */
-export interface ThrottlingSettings {
-  /** The round trip time in milliseconds. */
-  rttMs?: number;
-  /** The network throughput in kilobits per second. */
-  throughputKbps?: number;
-  // devtools settings
-  /** The network request latency in milliseconds. */
-  requestLatencyMs?: number;
-  /** The network download throughput in kilobits per second. */
-  downloadThroughputKbps?: number;
-  /** The network upload throughput in kilobits per second. */
-  uploadThroughputKbps?: number;
-  // used by both
-  /** The amount of slowdown applied to the cpu (1/<cpuSlowdownMultiplier>). */
-  cpuSlowdownMultiplier?: number
-}
-
-export interface PrecomputedLanternData {
-  additionalRttByOrigin: {[origin: string]: number};
-  serverResponseTimeByOrigin: {[origin: string]: number};
-}
-
-export type OutputMode = 'json' | 'html' | 'csv';
-
-export type ScreenEmulationSettings = {
-  /** Overriding width value in pixels (minimum 0, maximum 10000000). 0 disables the override. */
-  width: number;
-  /** Overriding height value in pixels (minimum 0, maximum 10000000). 0 disables the override. */
-  height: number;
-  /** Overriding device scale factor value. 0 disables the override. */
-  deviceScaleFactor: number;
-  /** Whether to emulate mobile device. This includes viewport meta tag, overlay scrollbars, text autosizing and more. */
-  mobile: boolean;
-  /** Whether screen emulation is disabled. If true, the other emulation settings are ignored. */
-  disabled: boolean;
-};
-
-/**
- * Options that are found in both the flags used by the Lighthouse module
- * interface and the Config's `settings` object.
- */
-export interface SharedFlagsSettings {
-  /** The type(s) of report output to be produced. */
-  output?: OutputMode|OutputMode[];
-  /** The locale to use for the output. */
-  locale?: Locale;
-  /** The maximum amount of time to wait for a page content render, in ms. If no content is rendered within this limit, the run is aborted with an error. */
-  maxWaitForFcp?: number;
-  /** The maximum amount of time to wait for a page to load, in ms. */
-  maxWaitForLoad?: number;
-  /** List of URL patterns to block. */
-  blockedUrlPatterns?: string[] | null;
-  /** Comma-delimited list of trace categories to include. */
-  additionalTraceCategories?: string | null;
-  /** Flag indicating the run should only audit. */
-  auditMode?: boolean | string;
-  /** Flag indicating the run should only gather. */
-  gatherMode?: boolean | string;
-  /** Flag indicating that the browser storage should not be reset for the audit. */
-  disableStorageReset?: boolean;
-  /** Flag indicating that Lighthouse should pause after page load to wait for the user's permission to continue the audit. */
-  debugNavigation?: boolean;
-
-  /** How Lighthouse should interpret this run in regards to scoring performance metrics and skipping mobile-only tests in desktop. Must be set even if throttling/emulation is being applied outside of Lighthouse. */
-  formFactor?: 'mobile'|'desktop';
-  /** Screen emulation properties (width, height, dpr, mobile viewport) to apply or an object of `{disabled: true}` if Lighthouse should avoid applying screen emulation. If either emulation is applied outside of Lighthouse, or it's being run on a mobile device, it typically should be set to disabled. For desktop, we recommend applying consistent desktop screen emulation. */
-  screenEmulation?: Partial<ScreenEmulationSettings>;
-  /** User Agent string to apply, `false` to not change the host's UA string, or `true` to use Lighthouse's default UA string. */
-  emulatedUserAgent?: string | boolean;
-
-  /** The method used to throttle the network. */
-  throttlingMethod?: 'devtools'|'simulate'|'provided';
-  /** The throttling config settings. */
-  throttling?: ThrottlingSettings;
-  /** If present, the run should only conduct this list of audits. */
-  onlyAudits?: string[] | null;
-  /** If present, the run should only conduct this list of categories. */
-  onlyCategories?: string[] | null;
-  /** If present, the run should skip this list of audits. */
-  skipAudits?: string[] | null;
-  /** List of extra HTTP Headers to include. */
-  extraHeaders?: Crdp.Network.Headers | null; // See extraHeaders TODO in bin.js
-  /** How Lighthouse was run, e.g. from the Chrome extension or from the npm module */
-  channel?: string
-  /** Precomputed lantern estimates to use instead of observed analysis. */
-  precomputedLanternData?: PrecomputedLanternData | null;
-  /** The budget.json object for LightWallet. */
-  budgets?: Array<Budget> | null;
-}
-
 /**
  * Extends the flags in SharedFlagsSettings with flags used to configure the
  * Lighthouse module but will not end up in the Config settings.
diff --git a/types/gatherer.d.ts b/types/gatherer.d.ts
index 8de6e370a..de92dd2ab 100644
--- a/types/gatherer.d.ts
+++ b/types/gatherer.d.ts
@@ -14,7 +14,7 @@ import ArbitraryEqualityMap = require('../lighthouse-core/lib/arbitrary-equality
 
 import {Artifacts, BaseArtifacts, GathererArtifacts} from './artifacts';
 import Config from './config';
-import {IcuMessage} from './i18n';
+import {IcuMessage} from './lhr/i18n';
 import Protocol from './protocol';
 import {Trace, DevtoolsLog} from './artifacts';
 
diff --git a/types/global-lh.d.ts b/types/global-lh.d.ts
index 7409e02c1..d8d07f73e 100644
--- a/types/global-lh.d.ts
+++ b/types/global-lh.d.ts
@@ -7,19 +7,20 @@
 import ArbitraryEqualityMap_ = require('../lighthouse-core/lib/arbitrary-equality-map.js');
 import * as Artifacts_ from './artifacts';
 import Audit_ from './audit';
-import Budget_ from './budget';
+import Budget_ from './lhr/budget';
 import Config_ from './config';
 import _Crdp from 'devtools-protocol/types/protocol';
 import _CrdpMappings from 'devtools-protocol/types/protocol-mapping';
 import * as Externs from './externs';
 import Gatherer_ from './gatherer';
-import * as I18n from './i18n';
+import * as I18n from './lhr/i18n';
 import LHError = require('../lighthouse-core/lib/lh-error.js');
-import LHResult from './lhr';
+import LHResult from './lhr/lhr';
 import Protocol_ from './protocol';
 import ReportResult_ from './html-renderer';
+import * as Settings from './lhr/settings';
 import StructuredData_ from './structured-data';
-import Treemap_ from './treemap';
+import Treemap_ from './lhr/treemap';
 
 // Construct hierarchy of global types under the LH namespace.
 declare global {
@@ -45,11 +46,6 @@ declare global {
     export import CrdpCommands = _CrdpMappings.Commands;
 
     // externs.d.ts
-    export import ThrottlingSettings = Externs.ThrottlingSettings;
-    export import ScreenEmulationSettings = Externs.ScreenEmulationSettings;
-    export import SharedFlagsSettings = Externs.SharedFlagsSettings;
-    export import OutputMode = Externs.OutputMode;
-    export import PrecomputedLanternData = Externs.PrecomputedLanternData;
     export import Flags = Externs.Flags;
     export import CliFlags = Externs.CliFlags;
     export import RunnerResult = Externs.RunnerResult;
@@ -60,15 +56,21 @@ declare global {
     export import Result = LHResult;
 
     // i18n.d.ts.
-    export import Locale = I18n.Locale;
     export import IcuMessage = I18n.IcuMessage;
     export import RawIcu = I18n.RawIcu;
     export import FormattedIcu = I18n.FormattedIcu;
-    export import IcuMessagePaths = I18n.IcuMessagePaths;
-    export import I18NRendererStrings = I18n.I18NRendererStrings;
 
     export import Protocol = Protocol_;
     export import ReportResult = ReportResult_;
+
+    // lhr/settings.d.ts
+    export import Locale = Settings.Locale;
+    export import ThrottlingSettings = Settings.ThrottlingSettings;
+    export import ScreenEmulationSettings = Settings.ScreenEmulationSettings;
+    export import SharedFlagsSettings = Settings.SharedFlagsSettings;
+    export import OutputMode = Settings.OutputMode;
+    export import PrecomputedLanternData = Settings.PrecomputedLanternData;
+
     export import StructuredData = StructuredData_;
     export import Treemap = Treemap_;
   }
diff --git a/types/html-renderer.d.ts b/types/html-renderer.d.ts
index 5a07565d2..7896ce2b3 100644
--- a/types/html-renderer.d.ts
+++ b/types/html-renderer.d.ts
@@ -5,7 +5,7 @@
  */
 
 import Audit from './audit';
-import LHResult from './lhr';
+import LHResult from './lhr/lhr';
 
 // Add needed DOM APIs not yet in tsc's lib dom.
 declare global {
diff --git a/types/audit-details.d.ts b/types/lhr/audit-details.d.ts
similarity index 96%
rename from types/audit-details.d.ts
rename to types/lhr/audit-details.d.ts
index 2b361709d..4d473efbf 100644
--- a/types/audit-details.d.ts
+++ b/types/lhr/audit-details.d.ts
@@ -6,7 +6,6 @@
 
 import {IcuMessage} from './i18n';
 import Treemap from './treemap';
-import {Artifacts} from './artifacts';
 
 type Details =
   Details.CriticalRequestChain |
@@ -83,8 +82,24 @@ declare module Details {
    * and the locations of interesting nodes.
    * Used by element screenshots renderer.
    */
-  interface FullPageScreenshot extends Artifacts.FullPageScreenshot {
+  interface FullPageScreenshot {
     type: 'full-page-screenshot';
+    screenshot: {
+      /** Base64 image data URL. */
+      data: string;
+      width: number;
+      height: number;
+    };
+    nodes: Record<string, Rect>;
+  }
+
+  interface Rect {
+    width: number;
+    height: number;
+    top: number;
+    right: number;
+    bottom: number;
+    left: number;
   }
 
   interface Table {
@@ -226,7 +241,7 @@ declare module Details {
     lhId?: string;
     path?: string;
     selector?: string;
-    boundingRect?: Artifacts.Rect;
+    boundingRect?: Rect;
     /** An HTML snippet used to identify the node. */
     snippet?: string;
     /** A human-friendly text descriptor that's used to identify the node more quickly. */
diff --git a/types/lhr/audit-result.d.ts b/types/lhr/audit-result.d.ts
new file mode 100644
index 000000000..41f4becca
--- /dev/null
+++ b/types/lhr/audit-result.d.ts
@@ -0,0 +1,59 @@
+/**
+ * @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.
+ */
+
+import {FormattedIcu} from './i18n';
+import AuditDetails from './audit-details';
+
+interface ScoreDisplayModes {
+  /** Scores of 0-1 (map to displayed scores of 0-100). */
+  NUMERIC: 'numeric';
+  /** Pass/fail audit (0 and 1 are only possible scores). */
+  BINARY: 'binary';
+  /** The audit exists only to tell you to review something yourself. Score is null and should be ignored. */
+  MANUAL: 'manual';
+  /** The audit is an FYI only, and can't be interpreted as pass/fail. Score is null and should be ignored. */
+  INFORMATIVE: 'informative';
+  /** The audit turned out to not apply to the page. Score is null and should be ignored. */
+  NOT_APPLICABLE: 'notApplicable';
+  /** There was an error while running the audit (check `errorMessage` for details). Score is null and should be ignored. */
+  ERROR: 'error';
+}
+
+type ScoreDisplayMode = ScoreDisplayModes[keyof ScoreDisplayModes];
+
+/** Audit result returned in Lighthouse report. All audits offer a description and score of 0-1. */
+export interface Result {
+  displayValue?: string;
+  /** An explanation of why the audit failed on the test page. */
+  explanation?: string;
+  /** Error message from any exception thrown while running this audit. */
+  errorMessage?: string;
+  warnings?: string[];
+  /** The scored value of the audit, provided in the range `0-1`, or null if `scoreDisplayMode` indicates not scored. */
+  score: number|null;
+  /**
+   * A string identifying how the score should be interpreted:
+   * - 'binary': pass/fail audit (0 and 1 are only possible scores).
+   * - 'numeric': scores of 0-1 (map to displayed scores of 0-100).
+   * - 'informative': the audit is an FYI only, and can't be interpreted as pass/fail. Score is null and should be ignored.
+   * - 'notApplicable': the audit turned out to not apply to the page. Score is null and should be ignored.
+   * - 'manual': The audit exists only to tell you to review something yourself. Score is null and should be ignored.
+   * - 'error': There was an error while running the audit (check `errorMessage` for details). Score is null and should be ignored.
+   */
+  scoreDisplayMode: ScoreDisplayMode;
+  /** Short, user-visible title for the audit. The text can change depending on if the audit passed or failed. */
+  title: string;
+  /** The string identifier of the audit, in kebab case. */
+  id: string;
+  /** A more detailed description that describes why the audit is important and links to Lighthouse documentation on the audit; markdown links supported. */
+  description: string;
+  /** A numeric value that has a meaning specific to the audit, e.g. the number of nodes in the DOM or the timestamp of a specific load event. More information can be found in the audit details, if present. */
+  numericValue?: number;
+  /** The unit of `numericValue`, used when the consumer wishes to convert numericValue to a display string. */
+  numericUnit?: string;
+  /** Extra information about the page provided by some types of audits, in one of several possible forms that can be rendered in the HTML report. */
+  details?: FormattedIcu<AuditDetails>;
+}
diff --git a/types/budget.d.ts b/types/lhr/budget.d.ts
similarity index 100%
rename from types/budget.d.ts
rename to types/lhr/budget.d.ts
diff --git a/types/i18n.d.ts b/types/lhr/i18n.d.ts
similarity index 62%
rename from types/i18n.d.ts
rename to types/lhr/i18n.d.ts
index e1606f147..aa9ed668d 100644
--- a/types/i18n.d.ts
+++ b/types/lhr/i18n.d.ts
@@ -4,9 +4,8 @@
  * 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.
  */
 
-import {Util} from '../report/renderer/util.js';
-
-export type Locale = 'en-US'|'en'|'en-AU'|'en-GB'|'en-IE'|'en-SG'|'en-ZA'|'en-IN'|'ar-XB'|'ar'|'bg'|'ca'|'cs'|'da'|'de'|'el'|'en-XA'|'en-XL'|'es'|'es-419'|'es-AR'|'es-BO'|'es-BR'|'es-BZ'|'es-CL'|'es-CO'|'es-CR'|'es-CU'|'es-DO'|'es-EC'|'es-GT'|'es-HN'|'es-MX'|'es-NI'|'es-PA'|'es-PE'|'es-PR'|'es-PY'|'es-SV'|'es-US'|'es-UY'|'es-VE'|'fi'|'fil'|'fr'|'he'|'hi'|'hr'|'hu'|'gsw'|'id'|'in'|'it'|'iw'|'ja'|'ko'|'lt'|'lv'|'mo'|'nl'|'nb'|'no'|'pl'|'pt'|'pt-PT'|'ro'|'ru'|'sk'|'sl'|'sr'|'sr-Latn'|'sv'|'ta'|'te'|'th'|'tl'|'tr'|'uk'|'vi'|'zh'|'zh-HK'|'zh-TW';
+// TODO(bckenny): i18n types should not be necessary for the LHR, but too difficult
+// to unravel from audit-details right now.
 
 export type IcuMessage = {
   // NOTE: `i18nId` rather than just `id` to make tsc typing easier (vs type branding which won't survive JSON roundtrip).
@@ -50,25 +49,3 @@ export type FormattedIcu<T> = T extends IcuMessage ?
   Exclude<T, IcuMessage> | string :
   // Otherwise recurse into any properties and repeat.
   {[K in keyof T]: FormattedIcu<T[K]>};
-
-/**
- * Info about an `LH.IcuMessage` value that was localized to a string. Value
- * is either a
- *  - path (`_.set()` style) into the original object where the message was replaced
- *  - those paths and a set of values that were inserted into the localized strings.
- */
-type IcuMessagePath = string | {path: string, values: Record<string, string | number>};
-
-/**
- * A representation of `LH.IcuMessage`s that were in an object (e.g. `LH.Result`)
- * and have been replaced by localized strings. `LH.IcuMessagePaths` provides a
- * mapping that can be used to change the locale of the object again.
- * Keyed by ids from the locale message json files, values are information on
- * how to replace the string if needed.
- */
-export interface IcuMessagePaths {
-  [i18nId: string]: IcuMessagePath[];
-}
-
-/** Localized strings for the html report renderer. */
-export type I18NRendererStrings = typeof Util['UIStrings'];
diff --git a/types/lhr.d.ts b/types/lhr/lhr.d.ts
similarity index 74%
rename from types/lhr.d.ts
rename to types/lhr/lhr.d.ts
index 4604ada8e..603305baf 100644
--- a/types/lhr.d.ts
+++ b/types/lhr/lhr.d.ts
@@ -4,10 +4,8 @@
  * 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.
  */
 
-import {Artifacts} from './artifacts';
-import Audit from './audit';
-import Config from './config';
-import {I18NRendererStrings, IcuMessagePaths} from './i18n';
+import {Result as AuditResult} from './audit-result';
+import {ConfigSettings} from './settings';
 
 /**
  * The full output of a Lighthouse run.
@@ -22,14 +20,14 @@ interface Result {
   /** The version of Lighthouse with which these results were generated. */
   lighthouseVersion: string;
   /** An object containing the results of the audits, keyed by the audits' `id` identifier. */
-  audits: Record<string, Audit.Result>;
+  audits: Record<string, AuditResult>;
   /** The top-level categories, their overall scores, and member audits. */
   categories: Record<string, Result.Category>;
   /** Descriptions of the groups referenced by CategoryMembers. */
   categoryGroups?: Record<string, Result.ReportGroup>;
 
   /** The config settings used for these results. */
-  configSettings: Config.Settings;
+  configSettings: ConfigSettings;
   /** List of top-level warnings for this Lighthouse run. */
   runWarnings: string[];
   /** A top-level error message that, if present, indicates a serious enough problem that this Lighthouse result may need to be discarded. */
@@ -40,8 +38,8 @@ interface Result {
   environment: Result.Environment;
   /** Execution timings for the Lighthouse run */
   timing: Result.Timing;
-  /** The record of all formatted string locations in the LHR and their corresponding source values. */
-  i18n: {rendererFormattedStrings: I18NRendererStrings, icuMessagePaths?: IcuMessagePaths};
+  /** Strings for the report and the record of all formatted string locations in the LHR and their corresponding source values. */
+  i18n: {rendererFormattedStrings: Record<string, string>, icuMessagePaths?: Result.IcuMessagePaths};
   /** An array containing the result of all stack packs. */
   stackPacks?: Result.StackPack[];
 }
@@ -60,10 +58,20 @@ declare module Result {
   }
 
   interface Timing {
-    entries: Artifacts.MeasureEntry[];
+    entries: MeasureEntry[];
     total: number;
   }
 
+  interface MeasureEntry {
+    // From PerformanceEntry
+    readonly duration: number;
+    readonly entryType: string;
+    readonly name: string;
+    readonly startTime: number;
+    /** Whether timing entry was collected during artifact gathering. */
+    gather?: boolean;
+  }
+
   interface Category {
     /** The string identifier of the category. */
     id: string;
@@ -114,6 +122,25 @@ declare module Result {
     /** A set of descriptions for some of Lighthouse's audits, keyed by audit `id`. */
     descriptions: Record<string, string>;
   }
+
+  /**
+   * Info about an `LH.IcuMessage` value that was localized to a string when
+   * included in the LHR. Value is either a
+   *  - path (`_.set()` style) into the original object where the message was replaced
+   *  - those paths and a set of values that were inserted into the localized strings.
+   */
+  type IcuMessagePath = string | {path: string, values: Record<string, string | number>};
+
+  /**
+   * A representation of `LH.IcuMessage`s that were in an object (e.g. `LH.Result`)
+   * and have been replaced by localized strings. `LH.Result.IcuMessagePaths` provides a
+   * mapping that can be used to change the locale of the object again.
+   * Keyed by ids from the locale message json files, values are information on
+   * how to replace the string if needed.
+   */
+  interface IcuMessagePaths {
+    [i18nId: string]: IcuMessagePath[];
+  }
 }
 
 export default Result;
diff --git a/types/lhr/settings.d.ts b/types/lhr/settings.d.ts
new file mode 100644
index 000000000..858ac6a62
--- /dev/null
+++ b/types/lhr/settings.d.ts
@@ -0,0 +1,105 @@
+/**
+ * @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.
+ */
+
+import Budget from './budget';
+
+export type Locale = 'en-US'|'en'|'en-AU'|'en-GB'|'en-IE'|'en-SG'|'en-ZA'|'en-IN'|'ar-XB'|'ar'|'bg'|'ca'|'cs'|'da'|'de'|'el'|'en-XA'|'en-XL'|'es'|'es-419'|'es-AR'|'es-BO'|'es-BR'|'es-BZ'|'es-CL'|'es-CO'|'es-CR'|'es-CU'|'es-DO'|'es-EC'|'es-GT'|'es-HN'|'es-MX'|'es-NI'|'es-PA'|'es-PE'|'es-PR'|'es-PY'|'es-SV'|'es-US'|'es-UY'|'es-VE'|'fi'|'fil'|'fr'|'he'|'hi'|'hr'|'hu'|'gsw'|'id'|'in'|'it'|'iw'|'ja'|'ko'|'lt'|'lv'|'mo'|'nl'|'nb'|'no'|'pl'|'pt'|'pt-PT'|'ro'|'ru'|'sk'|'sl'|'sr'|'sr-Latn'|'sv'|'ta'|'te'|'th'|'tl'|'tr'|'uk'|'vi'|'zh'|'zh-HK'|'zh-TW';
+
+/** Simulation settings that control the amount of network & cpu throttling in the run. */
+export interface ThrottlingSettings {
+  /** The round trip time in milliseconds. */
+  rttMs?: number;
+  /** The network throughput in kilobits per second. */
+  throughputKbps?: number;
+  // devtools settings
+  /** The network request latency in milliseconds. */
+  requestLatencyMs?: number;
+  /** The network download throughput in kilobits per second. */
+  downloadThroughputKbps?: number;
+  /** The network upload throughput in kilobits per second. */
+  uploadThroughputKbps?: number;
+  // used by both
+  /** The amount of slowdown applied to the cpu (1/<cpuSlowdownMultiplier>). */
+  cpuSlowdownMultiplier?: number
+}
+
+export interface PrecomputedLanternData {
+  additionalRttByOrigin: {[origin: string]: number};
+  serverResponseTimeByOrigin: {[origin: string]: number};
+}
+
+export type OutputMode = 'json' | 'html' | 'csv';
+
+export type ScreenEmulationSettings = {
+  /** Overriding width value in pixels (minimum 0, maximum 10000000). 0 disables the override. */
+  width: number;
+  /** Overriding height value in pixels (minimum 0, maximum 10000000). 0 disables the override. */
+  height: number;
+  /** Overriding device scale factor value. 0 disables the override. */
+  deviceScaleFactor: number;
+  /** Whether to emulate mobile device. This includes viewport meta tag, overlay scrollbars, text autosizing and more. */
+  mobile: boolean;
+  /** Whether screen emulation is disabled. If true, the other emulation settings are ignored. */
+  disabled: boolean;
+};
+
+/**
+ * Options that are found in both the flags used by the Lighthouse module
+ * interface and the Config's `settings` object.
+ */
+ export interface SharedFlagsSettings {
+  /** The type(s) of report output to be produced. */
+  output?: OutputMode|OutputMode[];
+  /** The locale to use for the output. */
+  locale?: Locale;
+  /** The maximum amount of time to wait for a page content render, in ms. If no content is rendered within this limit, the run is aborted with an error. */
+  maxWaitForFcp?: number;
+  /** The maximum amount of time to wait for a page to load, in ms. */
+  maxWaitForLoad?: number;
+  /** List of URL patterns to block. */
+  blockedUrlPatterns?: string[] | null;
+  /** Comma-delimited list of trace categories to include. */
+  additionalTraceCategories?: string | null;
+  /** Flag indicating the run should only audit. */
+  auditMode?: boolean | string;
+  /** Flag indicating the run should only gather. */
+  gatherMode?: boolean | string;
+  /** Flag indicating that the browser storage should not be reset for the audit. */
+  disableStorageReset?: boolean;
+  /** Flag indicating that Lighthouse should pause after page load to wait for the user's permission to continue the audit. */
+  debugNavigation?: boolean;
+
+  /** How Lighthouse should interpret this run in regards to scoring performance metrics and skipping mobile-only tests in desktop. Must be set even if throttling/emulation is being applied outside of Lighthouse. */
+  formFactor?: 'mobile'|'desktop';
+  /** Screen emulation properties (width, height, dpr, mobile viewport) to apply or an object of `{disabled: true}` if Lighthouse should avoid applying screen emulation. If either emulation is applied outside of Lighthouse, or it's being run on a mobile device, it typically should be set to disabled. For desktop, we recommend applying consistent desktop screen emulation. */
+  screenEmulation?: Partial<ScreenEmulationSettings>;
+  /** User Agent string to apply, `false` to not change the host's UA string, or `true` to use Lighthouse's default UA string. */
+  emulatedUserAgent?: string | boolean;
+
+  /** The method used to throttle the network. */
+  throttlingMethod?: 'devtools'|'simulate'|'provided';
+  /** The throttling config settings. */
+  throttling?: ThrottlingSettings;
+  /** If present, the run should only conduct this list of audits. */
+  onlyAudits?: string[] | null;
+  /** If present, the run should only conduct this list of categories. */
+  onlyCategories?: string[] | null;
+  /** If present, the run should skip this list of audits. */
+  skipAudits?: string[] | null;
+  /** List of extra HTTP Headers to include. */
+  extraHeaders?: Record<string, string> | null; // Matches `Protocol.Network.Headers`.
+  /** How Lighthouse was run, e.g. from the Chrome extension or from the npm module */
+  channel?: string
+  /** Precomputed lantern estimates to use instead of observed analysis. */
+  precomputedLanternData?: PrecomputedLanternData | null;
+  /** The budget.json object for LightWallet. */
+  budgets?: Array<Budget> | null;
+}
+
+export interface ConfigSettings extends Required<SharedFlagsSettings> {
+  throttling: Required<ThrottlingSettings>;
+  screenEmulation: ScreenEmulationSettings;
+}
diff --git a/types/treemap.d.ts b/types/lhr/treemap.d.ts
similarity index 92%
rename from types/treemap.d.ts
rename to types/lhr/treemap.d.ts
index f89232501..d03135710 100644
--- a/types/treemap.d.ts
+++ b/types/lhr/treemap.d.ts
@@ -4,8 +4,8 @@
  * 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.
  */
 
-import Audit from './audit';
-import {Locale} from './i18n';
+import {Result as AuditResult} from './audit-result';
+import {Locale} from './settings';
 
 declare module Treemap {
   interface Options {
@@ -13,7 +13,7 @@ declare module Treemap {
       requestedUrl: string;
       finalUrl: string;
       audits: {
-        'script-treemap-data': Audit.Result;
+        'script-treemap-data': AuditResult;
       };
       configSettings: {
         locale: Locale;
diff --git a/types/lhr/tsconfig.json b/types/lhr/tsconfig.json
new file mode 100644
index 000000000..6ec3bfbd0
--- /dev/null
+++ b/types/lhr/tsconfig.json
@@ -0,0 +1,24 @@
+{
+  "compilerOptions": {
+    "module": "esnext",
+    "target": "esnext",
+    "esModuleInterop": true,
+    "moduleResolution": "node",
+
+    "strict": true,
+    "extendedDiagnostics": true,
+
+    // We only need the base JS definitions, no libs in node_modules, no DOM, etc.
+    "lib": ["esnext"],
+    "types": [],
+
+    // "listFiles": true,
+    // "noErrorTruncation": true,
+
+    "composite": true,
+    "outDir": "../../.tmp/tsbuildinfo/lhr",
+  },
+  "include": [
+    "*.d.ts",
+  ],
+}
diff --git a/types/smokehouse.d.ts b/types/smokehouse.d.ts
index a9d84baf3..b6e5d840d 100644
--- a/types/smokehouse.d.ts
+++ b/types/smokehouse.d.ts
@@ -6,7 +6,7 @@
 
 import {Artifacts} from './artifacts';
 import Config from './config';
-import LHResult from './lhr';
+import LHResult from './lhr/lhr';
 
 declare global {
   module Smokehouse {


Test patch:
diff --git a/lighthouse-cli/test/smokehouse/frontends/smokehouse-bin.js b/lighthouse-cli/test/smokehouse/frontends/smokehouse-bin.js
index f33eed2f8..c2d7da9db 100644
--- a/lighthouse-cli/test/smokehouse/frontends/smokehouse-bin.js
+++ b/lighthouse-cli/test/smokehouse/frontends/smokehouse-bin.js
@@ -181,7 +181,7 @@ async function begin() {
   const invertMatch = argv.invertMatch;
   const testDefns = getDefinitionsToRun(allTestDefns, requestedTestIds, {invertMatch});
 
-  let isPassing;
+  let smokehouseResult;
   let server;
   let serverForOffline;
   let takeNetworkRequestUrls = undefined;
@@ -205,13 +205,19 @@ async function begin() {
       takeNetworkRequestUrls,
     };
 
-    isPassing = (await runSmokehouse(prunedTestDefns, options)).success;
+    smokehouseResult = (await runSmokehouse(prunedTestDefns, options));
   } finally {
     if (server) await server.close();
     if (serverForOffline) await serverForOffline.close();
   }
 
-  const exitCode = isPassing ? 0 : 1;
+  if (!smokehouseResult.success) {
+    const failedTestResults = smokehouseResult.testResults.filter(r => r.failed);
+    const cmd = `yarn smoke ${failedTestResults.map(r => r.id).join(' ')}`;
+    console.log(`rerun failures: ${cmd}`);
+  }
+
+  const exitCode = smokehouseResult.success ? 0 : 1;
   process.exit(exitCode);
 }
 
diff --git a/lighthouse-core/test/config/config-test.js b/lighthouse-core/test/config/config-test.js
index 0a05be625..b32a64cde 100644
--- a/lighthouse-core/test/config/config-test.js
+++ b/lighthouse-core/test/config/config-test.js
@@ -1242,7 +1242,7 @@ describe('Config', () => {
       assert.ok(config.audits.find(a => a.implementation.meta.id === 'full-page-screenshot'));
     });
 
-    it('should keep uncategorized audits even if onlyCategories is set', () => {
+    it('should keep full-page-screenshot even if onlyCategories is set', () => {
       assert.ok(origConfig.audits.includes('full-page-screenshot'));
       // full-page-screenshot does not belong to a category.
       const matchCategories = Object.values(origConfig.categories).filter(cat =>
@@ -1259,6 +1259,17 @@ describe('Config', () => {
 
       assert.ok(config.audits.find(a => a.implementation.meta.id === 'full-page-screenshot'));
     });
+
+    it('should keep full-page-screenshot even if skipAudits is set', () => {
+      const extended = {
+        extends: 'lighthouse:default',
+        settings: {
+          skipAudits: ['font-size'],
+        },
+      };
+      const config = new Config(extended);
+      assert.ok(config.audits.find(a => a.implementation.meta.id === 'full-page-screenshot'));
+    });
   });
 
   describe('#requireAudits', () => {
diff --git a/lighthouse-core/test/fraggle-rock/gather/session-test.js b/lighthouse-core/test/fraggle-rock/gather/session-test.js
index f04d4ec62..ed3e198dd 100644
--- a/lighthouse-core/test/fraggle-rock/gather/session-test.js
+++ b/lighthouse-core/test/fraggle-rock/gather/session-test.js
@@ -7,10 +7,19 @@
 
 const {EventEmitter} = require('events');
 const ProtocolSession = require('../../../fraggle-rock/gather/session.js');
+const {
+  flushAllTimersAndMicrotasks,
+  makePromiseInspectable,
+  createDecomposedPromise,
+} = require('../../test-utils.js');
 
 /* eslint-env jest */
 
+jest.useFakeTimers();
+
 describe('ProtocolSession', () => {
+  const DEFAULT_TIMEOUT = 30_000;
+
   /** @type {import('puppeteer').CDPSession} */
   let puppeteerSession;
   /** @type {ProtocolSession} */
@@ -18,7 +27,7 @@ describe('ProtocolSession', () => {
 
   beforeEach(() => {
     // @ts-expect-error - Individual mock functions are applied as necessary.
-    puppeteerSession = {emit: jest.fn()};
+    puppeteerSession = {emit: jest.fn(), send: jest.fn().mockResolvedValue()};
     session = new ProtocolSession(puppeteerSession);
   });
 
@@ -126,5 +135,92 @@ describe('ProtocolSession', () => {
       expect(result).toEqual(123);
       expect(send).toHaveBeenCalledWith('Page.navigate', {url: 'foo'});
     });
+
+    it('times out a request by default', async () => {
+      const sendPromise = createDecomposedPromise();
+      puppeteerSession.send = jest.fn().mockReturnValue(sendPromise.promise);
+
+      const resultPromise = makePromiseInspectable(session.sendCommand('Page.navigate', {url: ''}));
+
+      await jest.advanceTimersByTime(DEFAULT_TIMEOUT + 1);
+      await flushAllTimersAndMicrotasks();
+
+      expect(resultPromise).toBeDone();
+      await expect(resultPromise).rejects.toMatchObject({
+        code: 'PROTOCOL_TIMEOUT',
+        protocolMethod: 'Page.navigate',
+      });
+    });
+
+    it('times out a request with explicit timeout', async () => {
+      const sendPromise = createDecomposedPromise();
+      puppeteerSession.send = jest.fn().mockReturnValue(sendPromise.promise);
+
+      session.setNextProtocolTimeout(60_000);
+      const resultPromise = makePromiseInspectable(session.sendCommand('Page.navigate', {url: ''}));
+
+      await jest.advanceTimersByTime(DEFAULT_TIMEOUT + 1);
+      await flushAllTimersAndMicrotasks();
+
+      expect(resultPromise).not.toBeDone();
+
+      await jest.advanceTimersByTime(DEFAULT_TIMEOUT + 1);
+      await flushAllTimersAndMicrotasks();
+
+      expect(resultPromise).toBeDone();
+      await expect(resultPromise).rejects.toMatchObject({
+        code: 'PROTOCOL_TIMEOUT',
+        protocolMethod: 'Page.navigate',
+      });
+    });
+
+    it('respects a timeout of infinity', async () => {
+      const sendPromise = createDecomposedPromise();
+      puppeteerSession.send = jest.fn().mockReturnValue(sendPromise.promise);
+
+      session.setNextProtocolTimeout(Infinity);
+      const resultPromise = makePromiseInspectable(session.sendCommand('Page.navigate', {url: ''}));
+
+      await jest.advanceTimersByTime(100_000);
+      await flushAllTimersAndMicrotasks();
+
+      expect(resultPromise).not.toBeDone();
+
+      sendPromise.resolve('result');
+      await flushAllTimersAndMicrotasks();
+
+      expect(resultPromise).toBeDone();
+      expect(await resultPromise).toBe('result');
+    });
+  });
+
+  describe('.has/get/setNextProtocolTimeout', () => {
+    it('should handle when none has been set', () => {
+      expect(session.hasNextProtocolTimeout()).toBe(false);
+      expect(session.getNextProtocolTimeout()).toBe(DEFAULT_TIMEOUT);
+    });
+
+    it('should handle when one has been set', () => {
+      session.setNextProtocolTimeout(5_000);
+      expect(session.hasNextProtocolTimeout()).toBe(true);
+      expect(session.getNextProtocolTimeout()).toBe(5_000);
+    });
+
+    it('should handle when default has been explicitly set', () => {
+      session.setNextProtocolTimeout(DEFAULT_TIMEOUT);
+      expect(session.hasNextProtocolTimeout()).toBe(true);
+      expect(session.getNextProtocolTimeout()).toBe(DEFAULT_TIMEOUT);
+    });
+
+    it('should handle result after a command', () => {
+      session.setNextProtocolTimeout(10_000);
+      expect(session.hasNextProtocolTimeout()).toBe(true);
+      expect(session.getNextProtocolTimeout()).toBe(10_000);
+
+      session.sendCommand('Page.navigate', {url: ''});
+
+      expect(session.hasNextProtocolTimeout()).toBe(false);
+      expect(session.getNextProtocolTimeout()).toBe(DEFAULT_TIMEOUT);
+    });
   });
 });
diff --git a/lighthouse-core/test/gather/gather-runner-test.js b/lighthouse-core/test/gather/gather-runner-test.js
index 4605e3587..4e475b14d 100644
--- a/lighthouse-core/test/gather/gather-runner-test.js
+++ b/lighthouse-core/test/gather/gather-runner-test.js
@@ -1044,13 +1044,26 @@ describe('GatherRunner', function() {
         'warning0',
         'warning1',
         'warning2',
+        {
+          i18nId: 'LH.IcuMessage0',
+          formattedDefault: 'warning3',
+        },
+        {
+          i18nId: 'LH.IcuMessage1',
+          formattedDefault: 'warning4',
+        },
       ];
 
       class WarningGatherer extends Gatherer {
         /** @param {LH.Gatherer.PassContext} passContext */
         afterPass(passContext) {
-          passContext.LighthouseRunWarnings.push(...runWarnings, ...runWarnings);
-          assert.strictEqual(passContext.LighthouseRunWarnings.length, runWarnings.length * 2);
+          passContext.LighthouseRunWarnings.push(
+            ...runWarnings,
+            ...runWarnings,
+            // adding one of the warnings with keys in different order
+            {formattedDefault: 'warning3', i18nId: 'LH.IcuMessage0'}
+          );
+          assert.strictEqual(passContext.LighthouseRunWarnings.length, runWarnings.length * 2 + 1);
 
           return '';
         }