Repository: GoogleChrome/lighthouse
Pull request #12649 (2021-06-10 20:55:19)

core(fr): add audit mode filter
Implementation of our ideas in https://github.com/GoogleChrome/lighthouse/pull/12595#discussion_r645172157

This feels good to me, but I would be receptive to making `applicableModes` required for two reasons:
- It's more explicit about when the audit will be run.
- It would be helpful to throw an error if the available artifacts don't meet our expectations, instead of silently skipping any audits that have a missing artifact.

patrickhulce: > I would be receptive to making applicableModes required for two reasons:

Feels like a solid v9 breaking change to make :)

Patch:
diff --git a/lighthouse-core/audits/byte-efficiency/offscreen-images.js b/lighthouse-core/audits/byte-efficiency/offscreen-images.js
index 932cf4802..509f4c3f9 100644
--- a/lighthouse-core/audits/byte-efficiency/offscreen-images.js
+++ b/lighthouse-core/audits/byte-efficiency/offscreen-images.js
@@ -14,7 +14,7 @@ const Sentry = require('../../lib/sentry.js');
 const URL = require('../../lib/url-shim.js');
 const i18n = require('../../lib/i18n/i18n.js');
 const Interactive = require('../../computed/metrics/interactive.js');
-const TraceOfTab = require('../../computed/trace-of-tab.js');
+const ProcessedTrace = require('../../computed/processed-trace.js');
 
 const UIStrings = {
   /** Imperative title of a Lighthouse audit that tells the user to defer loading offscreen images. Offscreen images are images located outside of the visible browser viewport. As they are unseen by the user and slow down page load, they should be loaded later, closer to when the user is going to see them. This is displayed in a list of audit titles that Lighthouse generates. */
@@ -48,7 +48,8 @@ class OffscreenImages extends ByteEfficiencyAudit {
       title: str_(UIStrings.title),
       description: str_(UIStrings.description),
       scoreDisplayMode: ByteEfficiencyAudit.SCORING_MODES.NUMERIC,
-      requiredArtifacts: ['ImageElements', 'ViewportDimensions', 'devtoolsLogs', 'traces'],
+      requiredArtifacts: ['ImageElements', 'ViewportDimensions', 'GatherContext', 'devtoolsLogs',
+        'traces'],
     };
   }
 
@@ -181,6 +182,7 @@ class OffscreenImages extends ByteEfficiencyAudit {
   static async audit_(artifacts, networkRecords, context) {
     const images = artifacts.ImageElements;
     const viewportDimensions = artifacts.ViewportDimensions;
+    const gatherContext = artifacts.GatherContext;
     const trace = artifacts.traces[ByteEfficiencyAudit.DEFAULT_PASS];
     const devtoolsLog = artifacts.devtoolsLogs[ByteEfficiencyAudit.DEFAULT_PASS];
 
@@ -213,7 +215,8 @@ class OffscreenImages extends ByteEfficiencyAudit {
     const unfilteredResults = Array.from(resultsMap.values());
     // get the interactive time or fallback to getting the end of trace time
     try {
-      const interactive = await Interactive.request({trace, devtoolsLog, settings}, context);
+      const metricComputationData = {trace, devtoolsLog, gatherContext, settings};
+      const interactive = await Interactive.request(metricComputationData, context);
 
       // use interactive to generate items
       const lanternInteractive = /** @type {LH.Artifacts.LanternMetric} */ (interactive);
@@ -229,7 +232,7 @@ class OffscreenImages extends ByteEfficiencyAudit {
       }
       // use end of trace as a substitute for finding interactive time
       items = OffscreenImages.filterObservedResults(unfilteredResults,
-        await TraceOfTab.request(trace, context).then(tot => tot.timestamps.traceEnd));
+        await ProcessedTrace.request(trace, context).then(tot => tot.timestamps.traceEnd));
     }
 
     /** @type {LH.Audit.Details.Opportunity['headings']} */
diff --git a/lighthouse-core/audits/byte-efficiency/render-blocking-resources.js b/lighthouse-core/audits/byte-efficiency/render-blocking-resources.js
index 02a4bbba5..1ef4a2496 100644
--- a/lighthouse-core/audits/byte-efficiency/render-blocking-resources.js
+++ b/lighthouse-core/audits/byte-efficiency/render-blocking-resources.js
@@ -15,7 +15,8 @@ const BaseNode = require('../../lib/dependency-graph/base-node.js');
 const ByteEfficiencyAudit = require('./byte-efficiency-audit.js');
 const UnusedCSS = require('../../computed/unused-css.js');
 const NetworkRequest = require('../../lib/network-request.js');
-const TraceOfTab = require('../../computed/trace-of-tab.js');
+const ProcessedTrace = require('../../computed/processed-trace.js');
+const ProcessedNavigation = require('../../computed/processed-navigation.js');
 const LoadSimulator = require('../../computed/load-simulator.js');
 const FirstContentfulPaint = require('../../computed/metrics/first-contentful-paint.js');
 
@@ -117,7 +118,7 @@ class RenderBlockingResources extends Audit {
       // TODO: look into adding an `optionalArtifacts` property that captures the non-required nature
       // of CSSUsage
       requiredArtifacts: ['URL', 'TagsBlockingFirstPaint', 'traces', 'devtoolsLogs', 'CSSUsage',
-        'Stacks'],
+        'GatherContext', 'Stacks'],
     };
   }
 
@@ -127,23 +128,28 @@ class RenderBlockingResources extends Audit {
    * @return {Promise<{wastedMs: number, results: Array<{url: string, totalBytes: number, wastedMs: number}>}>}
    */
   static async computeResults(artifacts, context) {
+    const gatherContext = artifacts.GatherContext;
     const trace = artifacts.traces[Audit.DEFAULT_PASS];
     const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
     const simulatorData = {devtoolsLog, settings: context.settings};
-    const traceOfTab = await TraceOfTab.request(trace, context);
+    const processedTrace = await ProcessedTrace.request(trace, context);
+    const processedNavigation = await ProcessedNavigation.request(processedTrace, context);
     const simulator = await LoadSimulator.request(simulatorData, context);
     const wastedCssBytes = await RenderBlockingResources.computeWastedCSSBytes(artifacts, context);
 
-    const metricSettings = {throttlingMethod: 'simulate'};
+    /** @type {Immutable<LH.Config.Settings>} */
+    const metricSettings = {
+      ...context.settings,
+      throttlingMethod: 'simulate',
+    };
 
-    /** @type {LH.Artifacts.MetricComputationData} */
-    // @ts-expect-error - TODO(bckenny): allow optional `throttling` settings
-    const metricComputationData = {trace, devtoolsLog, simulator, settings: metricSettings};
+    const metricComputationData = {trace, devtoolsLog, gatherContext, simulator,
+      settings: metricSettings};
 
     // Cast to just `LanternMetric` since we explicitly set `throttlingMethod: 'simulate'`.
     const fcpSimulation = /** @type {LH.Artifacts.LanternMetric} */
       (await FirstContentfulPaint.request(metricComputationData, context));
-    const fcpTsInMs = traceOfTab.timestamps.firstContentfulPaint / 1000;
+    const fcpTsInMs = processedNavigation.timestamps.firstContentfulPaint / 1000;
 
     const nodesByUrl = getNodesAndTimingByUrl(fcpSimulation.optimisticEstimate.nodeTimings);
 
diff --git a/lighthouse-core/audits/final-screenshot.js b/lighthouse-core/audits/final-screenshot.js
index a36bd1625..9628f5ce1 100644
--- a/lighthouse-core/audits/final-screenshot.js
+++ b/lighthouse-core/audits/final-screenshot.js
@@ -7,7 +7,7 @@
 
 const Audit = require('./audit.js');
 const LHError = require('../lib/lh-error.js');
-const TraceOfTab = require('../computed/trace-of-tab.js');
+const ProcessedTrace = require('../computed/processed-trace.js');
 const Screenshots = require('../computed/screenshots.js');
 
 class FinalScreenshot extends Audit {
@@ -31,9 +31,9 @@ class FinalScreenshot extends Audit {
    */
   static async audit(artifacts, context) {
     const trace = artifacts.traces[Audit.DEFAULT_PASS];
-    const traceOfTab = await TraceOfTab.request(trace, context);
+    const processedTrace = await ProcessedTrace.request(trace, context);
     const screenshots = await Screenshots.request(trace, context);
-    const {timeOrigin} = traceOfTab.timestamps;
+    const {timeOrigin} = processedTrace.timestamps;
     const finalScreenshot = screenshots[screenshots.length - 1];
 
     if (!finalScreenshot) {
diff --git a/lighthouse-core/audits/long-tasks.js b/lighthouse-core/audits/long-tasks.js
index 94fd5ff3e..973bc7773 100644
--- a/lighthouse-core/audits/long-tasks.js
+++ b/lighthouse-core/audits/long-tasks.js
@@ -62,7 +62,7 @@ class LongTasks extends Audit {
     const taskTimingsByEvent = new Map();
 
     if (settings.throttlingMethod === 'simulate') {
-      const simulatorOptions = {trace, devtoolsLog, settings: context.settings};
+      const simulatorOptions = {devtoolsLog, settings: context.settings};
       const pageGraph = await PageDependencyGraph.request({trace, devtoolsLog}, context);
       const simulator = await LoadSimulator.request(simulatorOptions, context);
       const simulation = await simulator.simulate(pageGraph, {label: 'long-tasks-diagnostic'});
diff --git a/lighthouse-core/audits/metrics.js b/lighthouse-core/audits/metrics.js
index 2afb9c845..ea444e23e 100644
--- a/lighthouse-core/audits/metrics.js
+++ b/lighthouse-core/audits/metrics.js
@@ -28,7 +28,7 @@ class Metrics extends Audit {
       scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
       title: 'Metrics',
       description: 'Collects all available metrics.',
-      requiredArtifacts: ['traces', 'devtoolsLogs'],
+      requiredArtifacts: ['traces', 'devtoolsLogs', 'GatherContext'],
     };
   }
 
@@ -38,10 +38,11 @@ class Metrics extends Audit {
    * @return {Promise<LH.Audit.Product>}
    */
   static async audit(artifacts, context) {
+    const gatherContext = artifacts.GatherContext;
     const trace = artifacts.traces[Audit.DEFAULT_PASS];
     const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
     const summary = await ComputedTimingSummary
-      .request({trace, devtoolsLog, settings: context.settings}, context);
+      .request({trace, devtoolsLog, gatherContext, settings: context.settings}, context);
     const metrics = summary.metrics;
     const debugInfo = summary.debugInfo;
 
diff --git a/lighthouse-core/audits/metrics/first-contentful-paint-3g.js b/lighthouse-core/audits/metrics/first-contentful-paint-3g.js
index 768e0f7ef..317150c75 100644
--- a/lighthouse-core/audits/metrics/first-contentful-paint-3g.js
+++ b/lighthouse-core/audits/metrics/first-contentful-paint-3g.js
@@ -20,7 +20,7 @@ class FirstContentfulPaint3G extends Audit {
       description: 'First Contentful Paint 3G marks the time at which the first text or image is ' +
         `painted while on a 3G network. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/first-contentful-paint).`,
       scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
-      requiredArtifacts: ['traces', 'devtoolsLogs'],
+      requiredArtifacts: ['traces', 'devtoolsLogs', 'GatherContext'],
     };
   }
 
@@ -43,11 +43,12 @@ class FirstContentfulPaint3G extends Audit {
    * @return {Promise<LH.Audit.Product>}
    */
   static async audit(artifacts, context) {
+    const gatherContext = artifacts.GatherContext;
     const trace = artifacts.traces[Audit.DEFAULT_PASS];
     const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
     /** @type {Immutable<LH.Config.Settings>} */
     const settings = {...context.settings, throttlingMethod: 'simulate', throttling: regular3G};
-    const metricComputationData = {trace, devtoolsLog, settings};
+    const metricComputationData = {trace, devtoolsLog, gatherContext, settings};
     const metricResult = await ComputedFcp.request(metricComputationData, context);
 
     return {
diff --git a/lighthouse-core/audits/metrics/first-contentful-paint.js b/lighthouse-core/audits/metrics/first-contentful-paint.js
index c45aadd98..aa714d6a7 100644
--- a/lighthouse-core/audits/metrics/first-contentful-paint.js
+++ b/lighthouse-core/audits/metrics/first-contentful-paint.js
@@ -27,7 +27,7 @@ class FirstContentfulPaint extends Audit {
       title: str_(i18n.UIStrings.firstContentfulPaintMetric),
       description: str_(UIStrings.description),
       scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
-      requiredArtifacts: ['traces', 'devtoolsLogs'],
+      requiredArtifacts: ['traces', 'devtoolsLogs', 'GatherContext'],
     };
   }
 
@@ -63,7 +63,8 @@ class FirstContentfulPaint extends Audit {
   static async audit(artifacts, context) {
     const trace = artifacts.traces[Audit.DEFAULT_PASS];
     const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
-    const metricComputationData = {trace, devtoolsLog, settings: context.settings};
+    const gatherContext = artifacts.GatherContext;
+    const metricComputationData = {trace, devtoolsLog, gatherContext, settings: context.settings};
     const metricResult = await ComputedFcp.request(metricComputationData, context);
     const options = context.options[context.settings.formFactor];
 
diff --git a/lighthouse-core/audits/metrics/first-meaningful-paint.js b/lighthouse-core/audits/metrics/first-meaningful-paint.js
index cf6390cb1..733b0affd 100644
--- a/lighthouse-core/audits/metrics/first-meaningful-paint.js
+++ b/lighthouse-core/audits/metrics/first-meaningful-paint.js
@@ -27,7 +27,7 @@ class FirstMeaningfulPaint extends Audit {
       title: str_(i18n.UIStrings.firstMeaningfulPaintMetric),
       description: str_(UIStrings.description),
       scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
-      requiredArtifacts: ['traces', 'devtoolsLogs'],
+      requiredArtifacts: ['traces', 'devtoolsLogs', 'GatherContext'],
     };
   }
 
@@ -66,7 +66,8 @@ class FirstMeaningfulPaint extends Audit {
   static async audit(artifacts, context) {
     const trace = artifacts.traces[Audit.DEFAULT_PASS];
     const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
-    const metricComputationData = {trace, devtoolsLog, settings: context.settings};
+    const gatherContext = artifacts.GatherContext;
+    const metricComputationData = {trace, devtoolsLog, gatherContext, settings: context.settings};
     const metricResult = await ComputedFmp.request(metricComputationData, context);
     const options = context.options[context.settings.formFactor];
 
diff --git a/lighthouse-core/audits/metrics/interactive.js b/lighthouse-core/audits/metrics/interactive.js
index ad3f36bb8..fe3327d41 100644
--- a/lighthouse-core/audits/metrics/interactive.js
+++ b/lighthouse-core/audits/metrics/interactive.js
@@ -33,7 +33,7 @@ class InteractiveMetric extends Audit {
       title: str_(i18n.UIStrings.interactiveMetric),
       description: str_(UIStrings.description),
       scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
-      requiredArtifacts: ['traces', 'devtoolsLogs'],
+      requiredArtifacts: ['traces', 'devtoolsLogs', 'GatherContext'],
     };
   }
 
@@ -69,7 +69,8 @@ class InteractiveMetric extends Audit {
   static async audit(artifacts, context) {
     const trace = artifacts.traces[Audit.DEFAULT_PASS];
     const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
-    const metricComputationData = {trace, devtoolsLog, settings: context.settings};
+    const gatherContext = artifacts.GatherContext;
+    const metricComputationData = {trace, devtoolsLog, gatherContext, settings: context.settings};
     const metricResult = await Interactive.request(metricComputationData, context);
     const timeInMs = metricResult.timing;
     const options = context.options[context.settings.formFactor];
diff --git a/lighthouse-core/audits/metrics/largest-contentful-paint.js b/lighthouse-core/audits/metrics/largest-contentful-paint.js
index 0fa4f0c56..4b8afcc38 100644
--- a/lighthouse-core/audits/metrics/largest-contentful-paint.js
+++ b/lighthouse-core/audits/metrics/largest-contentful-paint.js
@@ -28,7 +28,7 @@ class LargestContentfulPaint extends Audit {
       title: str_(i18n.UIStrings.largestContentfulPaintMetric),
       description: str_(UIStrings.description),
       scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
-      requiredArtifacts: ['HostUserAgent', 'traces', 'devtoolsLogs'],
+      requiredArtifacts: ['HostUserAgent', 'traces', 'devtoolsLogs', 'GatherContext'],
     };
   }
 
@@ -72,7 +72,8 @@ class LargestContentfulPaint extends Audit {
   static async audit(artifacts, context) {
     const trace = artifacts.traces[Audit.DEFAULT_PASS];
     const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
-    const metricComputationData = {trace, devtoolsLog, settings: context.settings};
+    const gatherContext = artifacts.GatherContext;
+    const metricComputationData = {trace, devtoolsLog, gatherContext, settings: context.settings};
 
     let metricResult;
     try {
diff --git a/lighthouse-core/audits/metrics/max-potential-fid.js b/lighthouse-core/audits/metrics/max-potential-fid.js
index a8f3fd6dd..42036bb77 100644
--- a/lighthouse-core/audits/metrics/max-potential-fid.js
+++ b/lighthouse-core/audits/metrics/max-potential-fid.js
@@ -32,7 +32,7 @@ class MaxPotentialFID extends Audit {
       title: str_(i18n.UIStrings.maxPotentialFIDMetric),
       description: str_(UIStrings.description),
       scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
-      requiredArtifacts: ['traces', 'devtoolsLogs'],
+      requiredArtifacts: ['traces', 'devtoolsLogs', 'GatherContext'],
     };
   }
 
@@ -55,7 +55,8 @@ class MaxPotentialFID extends Audit {
   static async audit(artifacts, context) {
     const trace = artifacts.traces[Audit.DEFAULT_PASS];
     const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
-    const metricComputationData = {trace, devtoolsLog, settings: context.settings};
+    const gatherContext = artifacts.GatherContext;
+    const metricComputationData = {trace, devtoolsLog, gatherContext, settings: context.settings};
     const metricResult = await ComputedFid.request(metricComputationData, context);
 
     return {
diff --git a/lighthouse-core/audits/metrics/speed-index.js b/lighthouse-core/audits/metrics/speed-index.js
index c0b191752..49ab11403 100644
--- a/lighthouse-core/audits/metrics/speed-index.js
+++ b/lighthouse-core/audits/metrics/speed-index.js
@@ -27,7 +27,7 @@ class SpeedIndex extends Audit {
       title: str_(i18n.UIStrings.speedIndexMetric),
       description: str_(UIStrings.description),
       scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
-      requiredArtifacts: ['traces', 'devtoolsLogs'],
+      requiredArtifacts: ['traces', 'devtoolsLogs', 'GatherContext'],
     };
   }
 
@@ -65,7 +65,8 @@ class SpeedIndex extends Audit {
   static async audit(artifacts, context) {
     const trace = artifacts.traces[Audit.DEFAULT_PASS];
     const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
-    const metricComputationData = {trace, devtoolsLog, settings: context.settings};
+    const gatherContext = artifacts.GatherContext;
+    const metricComputationData = {trace, devtoolsLog, gatherContext, settings: context.settings};
     const metricResult = await ComputedSi.request(metricComputationData, context);
     const options = context.options[context.settings.formFactor];
 
diff --git a/lighthouse-core/audits/metrics/total-blocking-time.js b/lighthouse-core/audits/metrics/total-blocking-time.js
index 9806feee0..cb4c80179 100644
--- a/lighthouse-core/audits/metrics/total-blocking-time.js
+++ b/lighthouse-core/audits/metrics/total-blocking-time.js
@@ -27,7 +27,7 @@ class TotalBlockingTime extends Audit {
       title: str_(i18n.UIStrings.totalBlockingTimeMetric),
       description: str_(UIStrings.description),
       scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
-      requiredArtifacts: ['traces', 'devtoolsLogs'],
+      requiredArtifacts: ['traces', 'devtoolsLogs', 'GatherContext'],
     };
   }
 
@@ -87,7 +87,8 @@ class TotalBlockingTime extends Audit {
   static async audit(artifacts, context) {
     const trace = artifacts.traces[Audit.DEFAULT_PASS];
     const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
-    const metricComputationData = {trace, devtoolsLog, settings: context.settings};
+    const gatherContext = artifacts.GatherContext;
+    const metricComputationData = {trace, devtoolsLog, gatherContext, settings: context.settings};
     const metricResult = await ComputedTBT.request(metricComputationData, context);
 
     const options = context.options[context.settings.formFactor];
diff --git a/lighthouse-core/audits/predictive-perf.js b/lighthouse-core/audits/predictive-perf.js
index 4434d730b..236269c6a 100644
--- a/lighthouse-core/audits/predictive-perf.js
+++ b/lighthouse-core/audits/predictive-perf.js
@@ -33,7 +33,7 @@ class PredictivePerf extends Audit {
         'Predicted performance evaluates how your site will perform under ' +
         'a cellular connection on a mobile device.',
       scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
-      requiredArtifacts: ['traces', 'devtoolsLogs'],
+      requiredArtifacts: ['traces', 'devtoolsLogs', 'GatherContext'],
     };
   }
 
@@ -43,16 +43,18 @@ class PredictivePerf extends Audit {
    * @return {Promise<LH.Audit.Product>}
    */
   static async audit(artifacts, context) {
+    const gatherContext = artifacts.GatherContext;
     const trace = artifacts.traces[Audit.DEFAULT_PASS];
     const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
     /** @type {LH.Config.Settings} */
     // @ts-expect-error - TODO(bckenny): allow optional `throttling` settings
     const settings = {}; // Use default settings.
-    const fcp = await LanternFcp.request({trace, devtoolsLog, settings}, context);
-    const fmp = await LanternFmp.request({trace, devtoolsLog, settings}, context);
-    const tti = await LanternInteractive.request({trace, devtoolsLog, settings}, context);
-    const si = await LanternSpeedIndex.request({trace, devtoolsLog, settings}, context);
-    const lcp = await LanternLcp.request({trace, devtoolsLog, settings}, context);
+    const computationData = {trace, devtoolsLog, gatherContext, settings};
+    const fcp = await LanternFcp.request(computationData, context);
+    const fmp = await LanternFmp.request(computationData, context);
+    const tti = await LanternInteractive.request(computationData, context);
+    const si = await LanternSpeedIndex.request(computationData, context);
+    const lcp = await LanternLcp.request(computationData, context);
 
     const values = {
       roughEstimateOfFCP: fcp.timing,
diff --git a/lighthouse-core/audits/preload-lcp-image.js b/lighthouse-core/audits/preload-lcp-image.js
index e885b8b46..0c1758c14 100644
--- a/lighthouse-core/audits/preload-lcp-image.js
+++ b/lighthouse-core/audits/preload-lcp-image.js
@@ -32,7 +32,8 @@ class PreloadLCPImageAudit extends Audit {
       id: 'preload-lcp-image',
       title: str_(UIStrings.title),
       description: str_(UIStrings.description),
-      requiredArtifacts: ['traces', 'devtoolsLogs', 'URL', 'TraceElements', 'ImageElements'],
+      requiredArtifacts: ['traces', 'devtoolsLogs', 'GatherContext', 'URL', 'TraceElements',
+        'ImageElements'],
       scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
     };
   }
@@ -195,17 +196,18 @@ class PreloadLCPImageAudit extends Audit {
    * @return {Promise<LH.Audit.Product>}
    */
   static async audit(artifacts, context) {
+    const gatherContext = artifacts.GatherContext;
     const trace = artifacts.traces[PreloadLCPImageAudit.DEFAULT_PASS];
     const devtoolsLog = artifacts.devtoolsLogs[PreloadLCPImageAudit.DEFAULT_PASS];
     const URL = artifacts.URL;
-    const simulatorOptions = {trace, devtoolsLog, settings: context.settings};
+    const metricData = {trace, devtoolsLog, gatherContext, settings: context.settings};
     const lcpElement = artifacts.TraceElements
       .find(element => element.traceEventType === 'largest-contentful-paint');
 
     const [mainResource, lanternLCP, simulator] = await Promise.all([
       MainResource.request({devtoolsLog, URL}, context),
-      LanternLCP.request(simulatorOptions, context),
-      LoadSimulator.request(simulatorOptions, context),
+      LanternLCP.request(metricData, context),
+      LoadSimulator.request({devtoolsLog, settings: context.settings}, context),
     ]);
 
     const graph = lanternLCP.pessimisticGraph;
diff --git a/lighthouse-core/audits/redirects.js b/lighthouse-core/audits/redirects.js
index 2a71a61eb..7e635a84d 100644
--- a/lighthouse-core/audits/redirects.js
+++ b/lighthouse-core/audits/redirects.js
@@ -8,7 +8,7 @@
 const Audit = require('./audit.js');
 const UnusedBytes = require('./byte-efficiency/byte-efficiency-audit.js');
 const i18n = require('../lib/i18n/i18n.js');
-const TraceOfTab = require('../computed/trace-of-tab.js');
+const ProcessedTrace = require('../computed/processed-trace.js');
 const NetworkRecords = require('../computed/network-records.js');
 const MainResource = require('../computed/main-resource.js');
 const LanternInteractive = require('../computed/metrics/lantern-interactive.js');
@@ -32,7 +32,7 @@ class Redirects extends Audit {
       title: str_(UIStrings.title),
       description: str_(UIStrings.description),
       scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
-      requiredArtifacts: ['URL', 'devtoolsLogs', 'traces'],
+      requiredArtifacts: ['URL', 'GatherContext', 'devtoolsLogs', 'traces'],
     };
   }
 
@@ -49,15 +49,15 @@ class Redirects extends Audit {
    *
    * @param {LH.Artifacts.NetworkRequest} mainResource
    * @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
-   * @param {LH.Artifacts.TraceOfTab} traceOfTab
+   * @param {LH.Artifacts.ProcessedTrace} processedTrace
    * @return {Array<LH.Artifacts.NetworkRequest>}
    */
-  static getDocumentRequestChain(mainResource, networkRecords, traceOfTab) {
+  static getDocumentRequestChain(mainResource, networkRecords, processedTrace) {
     /** @type {Array<LH.Artifacts.NetworkRequest>} */
     const documentRequests = [];
 
     // Find all the document requests by examining navigation events and their redirects
-    for (const event of traceOfTab.processEvents) {
+    for (const event of processedTrace.processEvents) {
       if (event.name !== 'navigationStart') continue;
 
       const data = event.args.data || {};
@@ -86,12 +86,13 @@ class Redirects extends Audit {
     const settings = context.settings;
     const trace = artifacts.traces[Audit.DEFAULT_PASS];
     const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
+    const gatherContext = artifacts.GatherContext;
 
-    const traceOfTab = await TraceOfTab.request(trace, context);
+    const processedTrace = await ProcessedTrace.request(trace, context);
     const networkRecords = await NetworkRecords.request(devtoolsLog, context);
     const mainResource = await MainResource.request({URL: artifacts.URL, devtoolsLog}, context);
 
-    const metricComputationData = {trace, devtoolsLog, traceOfTab, networkRecords, settings};
+    const metricComputationData = {trace, devtoolsLog, gatherContext, settings};
     const metricResult = await LanternInteractive.request(metricComputationData, context);
 
     /** @type {Map<string, LH.Gatherer.Simulation.NodeTiming>} */
@@ -103,7 +104,7 @@ class Redirects extends Audit {
     }
 
     const documentRequests = Redirects.getDocumentRequestChain(
-      mainResource, networkRecords, traceOfTab);
+      mainResource, networkRecords, processedTrace);
 
     let totalWastedMs = 0;
     const tableRows = [];
diff --git a/lighthouse-core/audits/screenshot-thumbnails.js b/lighthouse-core/audits/screenshot-thumbnails.js
index c823261b0..a02ef5668 100644
--- a/lighthouse-core/audits/screenshot-thumbnails.js
+++ b/lighthouse-core/audits/screenshot-thumbnails.js
@@ -9,7 +9,6 @@ const Audit = require('./audit.js');
 const LHError = require('../lib/lh-error.js');
 const jpeg = require('jpeg-js');
 const Speedline = require('../computed/speedline.js');
-const Interactive = require('../computed/metrics/interactive.js');
 
 const NUMBER_OF_THUMBNAILS = 10;
 const THUMBNAIL_WIDTH = 120;
@@ -79,16 +78,7 @@ class ScreenshotThumbnails extends Audit {
     const speedline = await Speedline.request(trace, context);
 
     // Make the minimum time range 3s so sites that load super quickly don't get a single screenshot
-    let minimumTimelineDuration = context.options.minimumTimelineDuration || 3000;
-    // Ensure thumbnails cover the full range of the trace (TTI can be later than visually complete)
-    if (context.settings.throttlingMethod !== 'simulate') {
-      const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
-      const metricComputationData = {trace, devtoolsLog, settings: context.settings};
-      const tti = Interactive.request(metricComputationData, context);
-      try {
-        minimumTimelineDuration = Math.max((await tti).timing, minimumTimelineDuration);
-      } catch (_) {}
-    }
+    const minimumTimelineDuration = context.options.minimumTimelineDuration || 3000;
 
     const thumbnails = [];
     const analyzedFrames = speedline.frames.filter(frame => !frame.isProgressInterpolated());
diff --git a/lighthouse-core/audits/timing-budget.js b/lighthouse-core/audits/timing-budget.js
index 8e67637e8..4cebf60a2 100644
--- a/lighthouse-core/audits/timing-budget.js
+++ b/lighthouse-core/audits/timing-budget.js
@@ -36,7 +36,7 @@ class TimingBudget extends Audit {
       title: str_(UIStrings.title),
       description: str_(UIStrings.description),
       scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
-      requiredArtifacts: ['devtoolsLogs', 'traces', 'URL'],
+      requiredArtifacts: ['devtoolsLogs', 'traces', 'URL', 'GatherContext'],
     };
   }
 
@@ -136,10 +136,11 @@ class TimingBudget extends Audit {
    * @return {Promise<LH.Audit.Product>}
    */
   static async audit(artifacts, context) {
+    const gatherContext = artifacts.GatherContext;
     const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
     const trace = artifacts.traces[Audit.DEFAULT_PASS];
     const mainResource = await MainResource.request({URL: artifacts.URL, devtoolsLog}, context);
-    const data = {trace, devtoolsLog, settings: context.settings};
+    const data = {trace, devtoolsLog, gatherContext, settings: context.settings};
     const summary = (await TimingSummary.request(data, context)).metrics;
     const budget = Budget.getMatchingBudget(context.settings.budgets, mainResource.url);
 
diff --git a/lighthouse-core/audits/uses-rel-preconnect.js b/lighthouse-core/audits/uses-rel-preconnect.js
index 045ebddb8..a5ef43bdc 100644
--- a/lighthouse-core/audits/uses-rel-preconnect.js
+++ b/lighthouse-core/audits/uses-rel-preconnect.js
@@ -13,7 +13,8 @@ const i18n = require('../lib/i18n/i18n.js');
 const NetworkRecords = require('../computed/network-records.js');
 const MainResource = require('../computed/main-resource.js');
 const LoadSimulator = require('../computed/load-simulator.js');
-const TraceOfTab = require('../computed/trace-of-tab.js');
+const ProcessedTrace = require('../computed/processed-trace.js');
+const ProcessedNavigation = require('../computed/processed-navigation.js');
 const PageDependencyGraph = require('../computed/page-dependency-graph.js');
 const LanternLCP = require('../computed/metrics/lantern-largest-contentful-paint.js');
 
@@ -113,16 +114,19 @@ class UsesRelPreconnectAudit extends Audit {
     /** @type {Array<LH.IcuMessage>} */
     const warnings = [];
 
-    const [networkRecords, mainResource, loadSimulator, traceOfTab, pageGraph] = await Promise.all([
-      NetworkRecords.request(devtoolsLog, context),
-      MainResource.request({devtoolsLog, URL: artifacts.URL}, context),
-      LoadSimulator.request({devtoolsLog, settings}, context),
-      TraceOfTab.request(trace, context),
-      PageDependencyGraph.request({trace, devtoolsLog}, context),
-    ]);
+    const processedTrace = await ProcessedTrace.request(trace, context);
+
+    const [networkRecords, mainResource, loadSimulator, processedNavigation, pageGraph] =
+      await Promise.all([
+        NetworkRecords.request(devtoolsLog, context),
+        MainResource.request({devtoolsLog, URL: artifacts.URL}, context),
+        LoadSimulator.request({devtoolsLog, settings}, context),
+        ProcessedNavigation.request(processedTrace, context),
+        PageDependencyGraph.request({trace, devtoolsLog}, context),
+      ]);
 
     const {rtt, additionalRttByOrigin} = loadSimulator.getOptions();
-    const lcpGraph = await LanternLCP.getPessimisticGraph(pageGraph, traceOfTab);
+    const lcpGraph = await LanternLCP.getPessimisticGraph(pageGraph, processedNavigation);
     /** @type {Set<string>} */
     const lcpGraphURLs = new Set();
     lcpGraph.traverse(node => {
diff --git a/lighthouse-core/audits/uses-rel-preload.js b/lighthouse-core/audits/uses-rel-preload.js
index 26e1a333e..564ee9db4 100644
--- a/lighthouse-core/audits/uses-rel-preload.js
+++ b/lighthouse-core/audits/uses-rel-preload.js
@@ -212,7 +212,7 @@ class UsesRelPreloadAudit extends Audit {
     const trace = artifacts.traces[UsesRelPreloadAudit.DEFAULT_PASS];
     const devtoolsLog = artifacts.devtoolsLogs[UsesRelPreloadAudit.DEFAULT_PASS];
     const URL = artifacts.URL;
-    const simulatorOptions = {trace, devtoolsLog, settings: context.settings};
+    const simulatorOptions = {devtoolsLog, settings: context.settings};
 
     const [mainResource, graph, simulator] = await Promise.all([
       MainResource.request({devtoolsLog, URL}, context),
diff --git a/lighthouse-core/computed/main-thread-tasks.js b/lighthouse-core/computed/main-thread-tasks.js
index 4f27ae7d9..e7e2c1887 100644
--- a/lighthouse-core/computed/main-thread-tasks.js
+++ b/lighthouse-core/computed/main-thread-tasks.js
@@ -7,7 +7,7 @@
 
 const makeComputedArtifact = require('./computed-artifact.js');
 const MainThreadTasks_ = require('../lib/tracehouse/main-thread-tasks.js');
-const TraceOfTab = require('./trace-of-tab.js');
+const ProcessedTrace = require('./processed-trace.js');
 
 class MainThreadTasks {
   /**
@@ -16,7 +16,7 @@ class MainThreadTasks {
    * @return {Promise<Array<LH.Artifacts.TaskNode>>}
    */
   static async compute_(trace, context) {
-    const {mainThreadEvents, frames, timestamps} = await TraceOfTab.request(trace, context);
+    const {mainThreadEvents, frames, timestamps} = await ProcessedTrace.request(trace, context);
     return MainThreadTasks_.getMainThreadTasks(mainThreadEvents, frames, timestamps.traceEnd);
   }
 }
diff --git a/lighthouse-core/computed/metrics/cumulative-layout-shift.js b/lighthouse-core/computed/metrics/cumulative-layout-shift.js
index 110652d24..d49d2991f 100644
--- a/lighthouse-core/computed/metrics/cumulative-layout-shift.js
+++ b/lighthouse-core/computed/metrics/cumulative-layout-shift.js
@@ -6,7 +6,7 @@
 'use strict';
 
 const makeComputedArtifact = require('../computed-artifact.js');
-const TraceOfTab = require('../trace-of-tab.js');
+const ProcessedTrace = require('../processed-trace.js');
 const LHError = require('../../lib/lh-error.js');
 
 /** @typedef {{ts: number, isMainFrame: boolean, weightedScore: number}} LayoutShiftEvent */
@@ -108,10 +108,10 @@ class CumulativeLayoutShift {
    * @return {Promise<{cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number, totalCumulativeLayoutShift: number}>}
    */
   static async compute_(trace, context) {
-    const traceOfTab = await TraceOfTab.request(trace, context);
+    const processedTrace = await ProcessedTrace.request(trace, context);
 
     const allFrameShiftEvents =
-        CumulativeLayoutShift.getLayoutShiftEvents(traceOfTab.frameTreeEvents);
+        CumulativeLayoutShift.getLayoutShiftEvents(processedTrace.frameTreeEvents);
     const mainFrameShiftEvents = allFrameShiftEvents.filter(e => e.isMainFrame);
 
     // The original Cumulative Layout Shift metric, the sum of all main-frame shift events.
diff --git a/lighthouse-core/computed/metrics/first-contentful-paint-all-frames.js b/lighthouse-core/computed/metrics/first-contentful-paint-all-frames.js
index 6abc243d8..25c253781 100644
--- a/lighthouse-core/computed/metrics/first-contentful-paint-all-frames.js
+++ b/lighthouse-core/computed/metrics/first-contentful-paint-all-frames.js
@@ -6,9 +6,9 @@
 'use strict';
 
 const makeComputedArtifact = require('../computed-artifact.js');
-const ComputedMetric = require('./metric.js');
+const NavigationMetric = require('./navigation-metric.js');
 
-class FirstContentfulPaintAllFrames extends ComputedMetric {
+class FirstContentfulPaintAllFrames extends NavigationMetric {
   /**
    * @return {Promise<LH.Artifacts.LanternMetric>}
    */
@@ -18,15 +18,15 @@ class FirstContentfulPaintAllFrames extends ComputedMetric {
   }
 
   /**
-   * @param {LH.Artifacts.MetricComputationData} data
+   * @param {LH.Artifacts.NavigationMetricComputationData} data
    * @return {Promise<LH.Artifacts.Metric>}
    */
   static async computeObservedMetric(data) {
-    const {traceOfTab} = data;
+    const {processedNavigation} = data;
 
     return {
-      timing: traceOfTab.timings.firstContentfulPaintAllFrames,
-      timestamp: traceOfTab.timestamps.firstContentfulPaintAllFrames,
+      timing: processedNavigation.timings.firstContentfulPaintAllFrames,
+      timestamp: processedNavigation.timestamps.firstContentfulPaintAllFrames,
     };
   }
 }
diff --git a/lighthouse-core/computed/metrics/first-contentful-paint.js b/lighthouse-core/computed/metrics/first-contentful-paint.js
index 1b0346837..7fd0f0cc3 100644
--- a/lighthouse-core/computed/metrics/first-contentful-paint.js
+++ b/lighthouse-core/computed/metrics/first-contentful-paint.js
@@ -6,29 +6,30 @@
 'use strict';
 
 const makeComputedArtifact = require('../computed-artifact.js');
-const ComputedMetric = require('./metric.js');
+const NavigationMetric = require('./navigation-metric.js');
 const LanternFirstContentfulPaint = require('./lantern-first-contentful-paint.js');
 
-class FirstContentfulPaint extends ComputedMetric {
+class FirstContentfulPaint extends NavigationMetric {
   /**
-   * @param {LH.Artifacts.MetricComputationData} data
+   * @param {LH.Artifacts.NavigationMetricComputationData} data
    * @param {LH.Artifacts.ComputedContext} context
    * @return {Promise<LH.Artifacts.LanternMetric>}
    */
   static computeSimulatedMetric(data, context) {
-    return LanternFirstContentfulPaint.request(data, context);
+    const metricData = NavigationMetric.getMetricComputationInput(data);
+    return LanternFirstContentfulPaint.request(metricData, context);
   }
 
   /**
-   * @param {LH.Artifacts.MetricComputationData} data
+   * @param {LH.Artifacts.NavigationMetricComputationData} data
    * @return {Promise<LH.Artifacts.Metric>}
    */
   static async computeObservedMetric(data) {
-    const {traceOfTab} = data;
+    const {processedNavigation} = data;
 
     return {
-      timing: traceOfTab.timings.firstContentfulPaint,
-      timestamp: traceOfTab.timestamps.firstContentfulPaint,
+      timing: processedNavigation.timings.firstContentfulPaint,
+      timestamp: processedNavigation.timestamps.firstContentfulPaint,
     };
   }
 }
diff --git a/lighthouse-core/computed/metrics/first-meaningful-paint.js b/lighthouse-core/computed/metrics/first-meaningful-paint.js
index d952979ab..592a2004c 100644
--- a/lighthouse-core/computed/metrics/first-meaningful-paint.js
+++ b/lighthouse-core/computed/metrics/first-meaningful-paint.js
@@ -6,33 +6,34 @@
 'use strict';
 
 const makeComputedArtifact = require('../computed-artifact.js');
-const ComputedMetric = require('./metric.js');
+const NavigationMetric = require('./navigation-metric.js');
 const LHError = require('../../lib/lh-error.js');
 const LanternFirstMeaningfulPaint = require('./lantern-first-meaningful-paint.js');
 
-class FirstMeaningfulPaint extends ComputedMetric {
+class FirstMeaningfulPaint extends NavigationMetric {
   /**
-   * @param {LH.Artifacts.MetricComputationData} data
+   * @param {LH.Artifacts.NavigationMetricComputationData} data
    * @param {LH.Artifacts.ComputedContext} context
    * @return {Promise<LH.Artifacts.LanternMetric>}
    */
   static computeSimulatedMetric(data, context) {
-    return LanternFirstMeaningfulPaint.request(data, context);
+    const metricData = NavigationMetric.getMetricComputationInput(data);
+    return LanternFirstMeaningfulPaint.request(metricData, context);
   }
 
   /**
-   * @param {LH.Artifacts.MetricComputationData} data
+   * @param {LH.Artifacts.NavigationMetricComputationData} data
    * @return {Promise<LH.Artifacts.Metric>}
    */
   static async computeObservedMetric(data) {
-    const {traceOfTab} = data;
-    if (traceOfTab.timings.firstMeaningfulPaint === undefined) {
+    const {processedNavigation} = data;
+    if (processedNavigation.timings.firstMeaningfulPaint === undefined) {
       throw new LHError(LHError.errors.NO_FMP);
     }
 
     return {
-      timing: traceOfTab.timings.firstMeaningfulPaint,
-      timestamp: traceOfTab.timestamps.firstMeaningfulPaint,
+      timing: processedNavigation.timings.firstMeaningfulPaint,
+      timestamp: processedNavigation.timestamps.firstMeaningfulPaint,
     };
   }
 }
diff --git a/lighthouse-core/computed/metrics/interactive.js b/lighthouse-core/computed/metrics/interactive.js
index 414888013..3b188e4b9 100644
--- a/lighthouse-core/computed/metrics/interactive.js
+++ b/lighthouse-core/computed/metrics/interactive.js
@@ -6,7 +6,7 @@
 'use strict';
 
 const makeComputedArtifact = require('../computed-artifact.js');
-const ComputedMetric = require('./metric.js');
+const NavigationMetric = require('./navigation-metric.js');
 const LanternInteractive = require('./lantern-interactive.js');
 
 const NetworkMonitor = require('../../gather/driver/network-monitor.js');
@@ -21,16 +21,16 @@ const ALLOWED_CONCURRENT_REQUESTS = 2;
  * resources and is mostly idle.
  * @see https://docs.google.com/document/d/1yE4YWsusi5wVXrnwhR61j-QyjK9tzENIzfxrCjA1NAk/edit#heading=h.yozfsuqcgpc4
  */
-class Interactive extends ComputedMetric {
+class Interactive extends NavigationMetric {
   /**
    * Finds all time periods where the number of inflight requests is less than or equal to the
    * number of allowed concurrent requests (2).
    * @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
-   * @param {{timestamps: {traceEnd: number}}} traceOfTab
+   * @param {{timestamps: {traceEnd: number}}} processedNavigation
    * @return {Array<TimePeriod>}
    */
-  static _findNetworkQuietPeriods(networkRecords, traceOfTab) {
-    const traceEndTsInMs = traceOfTab.timestamps.traceEnd / 1000;
+  static _findNetworkQuietPeriods(networkRecords, processedNavigation) {
+    const traceEndTsInMs = processedNavigation.timestamps.traceEnd / 1000;
     // Ignore records that failed, never finished, or were POST/PUT/etc.
     const filteredNetworkRecords = networkRecords.filter(record => {
       return record.finished && record.requestMethod === 'GET' && !record.failed &&
@@ -44,12 +44,12 @@ class Interactive extends ComputedMetric {
   /**
    * Finds all time periods where there are no long tasks.
    * @param {Array<TimePeriod>} longTasks
-   * @param {{timestamps: {timeOrigin: number, traceEnd: number}}} traceOfTab
+   * @param {{timestamps: {timeOrigin: number, traceEnd: number}}} processedNavigation
    * @return {Array<TimePeriod>}
    */
-  static _findCPUQuietPeriods(longTasks, traceOfTab) {
-    const timeOriginTsInMs = traceOfTab.timestamps.timeOrigin / 1000;
-    const traceEndTsInMs = traceOfTab.timestamps.traceEnd / 1000;
+  static _findCPUQuietPeriods(longTasks, processedNavigation) {
+    const timeOriginTsInMs = processedNavigation.timestamps.timeOrigin / 1000;
+    const traceEndTsInMs = processedNavigation.timestamps.traceEnd / 1000;
     if (longTasks.length === 0) {
       return [{start: 0, end: traceEndTsInMs}];
     }
@@ -84,19 +84,19 @@ class Interactive extends ComputedMetric {
    * Finds the first time period where a network quiet period and a CPU quiet period overlap.
    * @param {Array<TimePeriod>} longTasks
    * @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
-   * @param {LH.Artifacts.TraceOfTab} traceOfTab
+   * @param {LH.Artifacts.ProcessedNavigation} processedNavigation
    * @return {{cpuQuietPeriod: TimePeriod, networkQuietPeriod: TimePeriod, cpuQuietPeriods: Array<TimePeriod>, networkQuietPeriods: Array<TimePeriod>}}
    */
-  static findOverlappingQuietPeriods(longTasks, networkRecords, traceOfTab) {
-    const FcpTsInMs = traceOfTab.timestamps.firstContentfulPaint / 1000;
+  static findOverlappingQuietPeriods(longTasks, networkRecords, processedNavigation) {
+    const FcpTsInMs = processedNavigation.timestamps.firstContentfulPaint / 1000;
 
     /** @type {function(TimePeriod):boolean} */
     const isLongEnoughQuietPeriod = period =>
         period.end > FcpTsInMs + REQUIRED_QUIET_WINDOW &&
         period.end - period.start >= REQUIRED_QUIET_WINDOW;
-    const networkQuietPeriods = this._findNetworkQuietPeriods(networkRecords, traceOfTab)
+    const networkQuietPeriods = this._findNetworkQuietPeriods(networkRecords, processedNavigation)
         .filter(isLongEnoughQuietPeriod);
-    const cpuQuietPeriods = this._findCPUQuietPeriods(longTasks, traceOfTab)
+    const cpuQuietPeriods = this._findCPUQuietPeriods(longTasks, processedNavigation)
         .filter(isLongEnoughQuietPeriod);
 
     const cpuQueue = cpuQuietPeriods.slice();
@@ -141,41 +141,42 @@ class Interactive extends ComputedMetric {
   }
 
   /**
-   * @param {LH.Artifacts.MetricComputationData} data
+   * @param {LH.Artifacts.NavigationMetricComputationData} data
    * @param {LH.Artifacts.ComputedContext} context
    * @return {Promise<LH.Artifacts.LanternMetric>}
    */
   static computeSimulatedMetric(data, context) {
-    return LanternInteractive.request(data, context);
+    const metricData = NavigationMetric.getMetricComputationInput(data);
+    return LanternInteractive.request(metricData, context);
   }
 
   /**
-   * @param {LH.Artifacts.MetricComputationData} data
+   * @param {LH.Artifacts.NavigationMetricComputationData} data
    * @return {Promise<LH.Artifacts.Metric>}
    */
   static computeObservedMetric(data) {
-    const {traceOfTab, networkRecords} = data;
+    const {processedTrace, processedNavigation, networkRecords} = data;
 
-    if (!traceOfTab.timestamps.domContentLoaded) {
+    if (!processedNavigation.timestamps.domContentLoaded) {
       throw new LHError(LHError.errors.NO_DCL);
     }
 
-    const longTasks = TracingProcessor.getMainThreadTopLevelEvents(traceOfTab)
+    const longTasks = TracingProcessor.getMainThreadTopLevelEvents(processedTrace)
         .filter(event => event.duration >= 50);
     const quietPeriodInfo = Interactive.findOverlappingQuietPeriods(
       longTasks,
       networkRecords,
-      traceOfTab
+      processedNavigation
     );
 
     const cpuQuietPeriod = quietPeriodInfo.cpuQuietPeriod;
 
     const timestamp = Math.max(
       cpuQuietPeriod.start,
-      traceOfTab.timestamps.firstContentfulPaint / 1000,
-      traceOfTab.timestamps.domContentLoaded / 1000
+      processedNavigation.timestamps.firstContentfulPaint / 1000,
+      processedNavigation.timestamps.domContentLoaded / 1000
     ) * 1000;
-    const timing = (timestamp - traceOfTab.timestamps.timeOrigin) / 1000;
+    const timing = (timestamp - processedNavigation.timestamps.timeOrigin) / 1000;
     return Promise.resolve({timing, timestamp});
   }
 }
diff --git a/lighthouse-core/computed/metrics/lantern-first-contentful-paint.js b/lighthouse-core/computed/metrics/lantern-first-contentful-paint.js
index 608c858f2..58ff77d9a 100644
--- a/lighthouse-core/computed/metrics/lantern-first-contentful-paint.js
+++ b/lighthouse-core/computed/metrics/lantern-first-contentful-paint.js
@@ -168,13 +168,13 @@ class LanternFirstContentfulPaint extends LanternMetric {
 
   /**
    * @param {Node} dependencyGraph
-   * @param {LH.Artifacts.TraceOfTab} traceOfTab
+   * @param {LH.Artifacts.ProcessedNavigation} processedNavigation
    * @return {Node}
    */
-  static getOptimisticGraph(dependencyGraph, traceOfTab) {
+  static getOptimisticGraph(dependencyGraph, processedNavigation) {
     return this.getFirstPaintBasedGraph(
       dependencyGraph,
-      traceOfTab.timestamps.firstContentfulPaint,
+      processedNavigation.timestamps.firstContentfulPaint,
       // In the optimistic graph we exclude resources that appeared to be render blocking but were
       // initiated by a script. While they typically have a very high importance and tend to have a
       // significant impact on the page's content, these resources don't technically block rendering.
@@ -184,13 +184,13 @@ class LanternFirstContentfulPaint extends LanternMetric {
 
   /**
    * @param {Node} dependencyGraph
-   * @param {LH.Artifacts.TraceOfTab} traceOfTab
+   * @param {LH.Artifacts.ProcessedNavigation} processedNavigation
    * @return {Node}
    */
-  static getPessimisticGraph(dependencyGraph, traceOfTab) {
+  static getPessimisticGraph(dependencyGraph, processedNavigation) {
     return this.getFirstPaintBasedGraph(
       dependencyGraph,
-      traceOfTab.timestamps.firstContentfulPaint,
+      processedNavigation.timestamps.firstContentfulPaint,
       node => node.hasRenderBlockingPriority()
     );
   }
diff --git a/lighthouse-core/computed/metrics/lantern-first-meaningful-paint.js b/lighthouse-core/computed/metrics/lantern-first-meaningful-paint.js
index 1abaa8dd5..ceefc5c47 100644
--- a/lighthouse-core/computed/metrics/lantern-first-meaningful-paint.js
+++ b/lighthouse-core/computed/metrics/lantern-first-meaningful-paint.js
@@ -26,11 +26,11 @@ class LanternFirstMeaningfulPaint extends LanternMetric {
 
   /**
    * @param {Node} dependencyGraph
-   * @param {LH.Artifacts.TraceOfTab} traceOfTab
+   * @param {LH.Artifacts.ProcessedNavigation} processedNavigation
    * @return {Node}
    */
-  static getOptimisticGraph(dependencyGraph, traceOfTab) {
-    const fmp = traceOfTab.timestamps.firstMeaningfulPaint;
+  static getOptimisticGraph(dependencyGraph, processedNavigation) {
+    const fmp = processedNavigation.timestamps.firstMeaningfulPaint;
     if (!fmp) {
       throw new LHError(LHError.errors.NO_FMP);
     }
@@ -46,11 +46,11 @@ class LanternFirstMeaningfulPaint extends LanternMetric {
 
   /**
    * @param {Node} dependencyGraph
-   * @param {LH.Artifacts.TraceOfTab} traceOfTab
+   * @param {LH.Artifacts.ProcessedNavigation} processedNavigation
    * @return {Node}
    */
-  static getPessimisticGraph(dependencyGraph, traceOfTab) {
-    const fmp = traceOfTab.timestamps.firstMeaningfulPaint;
+  static getPessimisticGraph(dependencyGraph, processedNavigation) {
+    const fmp = processedNavigation.timestamps.firstMeaningfulPaint;
     if (!fmp) {
       throw new LHError(LHError.errors.NO_FMP);
     }
diff --git a/lighthouse-core/computed/metrics/lantern-largest-contentful-paint.js b/lighthouse-core/computed/metrics/lantern-largest-contentful-paint.js
index d0b3d529f..3d10320ed 100644
--- a/lighthouse-core/computed/metrics/lantern-largest-contentful-paint.js
+++ b/lighthouse-core/computed/metrics/lantern-largest-contentful-paint.js
@@ -41,11 +41,11 @@ class LanternLargestContentfulPaint extends LanternMetric {
 
   /**
    * @param {Node} dependencyGraph
-   * @param {LH.Artifacts.TraceOfTab} traceOfTab
+   * @param {LH.Artifacts.ProcessedNavigation} processedNavigation
    * @return {Node}
    */
-  static getOptimisticGraph(dependencyGraph, traceOfTab) {
-    const lcp = traceOfTab.timestamps.largestContentfulPaint;
+  static getOptimisticGraph(dependencyGraph, processedNavigation) {
+    const lcp = processedNavigation.timestamps.largestContentfulPaint;
     if (!lcp) {
       throw new LHError(LHError.errors.NO_LCP);
     }
@@ -59,11 +59,11 @@ class LanternLargestContentfulPaint extends LanternMetric {
 
   /**
    * @param {Node} dependencyGraph
-   * @param {LH.Artifacts.TraceOfTab} traceOfTab
+   * @param {LH.Artifacts.ProcessedNavigation} processedNavigation
    * @return {Node}
    */
-  static getPessimisticGraph(dependencyGraph, traceOfTab) {
-    const lcp = traceOfTab.timestamps.largestContentfulPaint;
+  static getPessimisticGraph(dependencyGraph, processedNavigation) {
+    const lcp = processedNavigation.timestamps.largestContentfulPaint;
     if (!lcp) {
       throw new LHError(LHError.errors.NO_LCP);
     }
diff --git a/lighthouse-core/computed/metrics/lantern-metric.js b/lighthouse-core/computed/metrics/lantern-metric.js
index a1ee6f470..906e3bd81 100644
--- a/lighthouse-core/computed/metrics/lantern-metric.js
+++ b/lighthouse-core/computed/metrics/lantern-metric.js
@@ -7,7 +7,8 @@
 
 const BaseNode = require('../../lib/dependency-graph/base-node.js');
 const NetworkRequest = require('../../lib/network-request.js');
-const TraceOfTab = require('../trace-of-tab.js');
+const ProcessedTrace = require('../processed-trace.js');
+const ProcessedNavigation = require('../processed-navigation.js');
 const PageDependencyGraph = require('../page-dependency-graph.js');
 const LoadSimulator = require('../load-simulator.js');
 
@@ -66,19 +67,19 @@ class LanternMetricArtifact {
 
   /**
    * @param {Node} dependencyGraph
-   * @param {LH.Artifacts.TraceOfTab} traceOfTab
+   * @param {LH.Artifacts.ProcessedNavigation} processedNavigation
    * @return {Node}
    */
-  static getOptimisticGraph(dependencyGraph, traceOfTab) { // eslint-disable-line no-unused-vars
+  static getOptimisticGraph(dependencyGraph, processedNavigation) { // eslint-disable-line no-unused-vars
     throw new Error('Optimistic graph unimplemented!');
   }
 
   /**
    * @param {Node} dependencyGraph
-   * @param {LH.Artifacts.TraceOfTab} traceOfTab
+   * @param {LH.Artifacts.ProcessedNavigation} processedNavigation
    * @return {Node}
    */
-  static getPessimisticGraph(dependencyGraph, traceOfTab) { // eslint-disable-line no-unused-vars
+  static getPessimisticGraph(dependencyGraph, processedNavigation) { // eslint-disable-line no-unused-vars
     throw new Error('Pessmistic graph unimplemented!');
   }
 
@@ -98,15 +99,22 @@ class LanternMetricArtifact {
    * @return {Promise<LH.Artifacts.LanternMetric>}
    */
   static async computeMetricWithGraphs(data, context, extras) {
+    // TODO: remove this fallback when lighthouse-pub-ads plugin can update.
+    const gatherContext = data.gatherContext || {gatherMode: 'navigation'};
     const {trace, devtoolsLog, settings} = data;
+    if (gatherContext.gatherMode !== 'navigation') {
+      throw new Error(`Lantern metrics can only be computed on navigations`);
+    }
+
     const metricName = this.name.replace('Lantern', '');
     const graph = await PageDependencyGraph.request({trace, devtoolsLog}, context);
-    const traceOfTab = await TraceOfTab.request(trace, context);
+    const processedTrace = await ProcessedTrace.request(trace, context);
+    const processedNavigation = await ProcessedNavigation.request(processedTrace, context);
     const simulator = data.simulator ||
         await LoadSimulator.request({devtoolsLog, settings}, context);
 
-    const optimisticGraph = this.getOptimisticGraph(graph, traceOfTab);
-    const pessimisticGraph = this.getPessimisticGraph(graph, traceOfTab);
+    const optimisticGraph = this.getOptimisticGraph(graph, processedNavigation);
+    const pessimisticGraph = this.getPessimisticGraph(graph, processedNavigation);
 
     /** @type {{flexibleOrdering?: boolean, label?: string}} */
     let simulateOptions = {label: `optimistic${metricName}`};
diff --git a/lighthouse-core/computed/metrics/largest-contentful-paint-all-frames.js b/lighthouse-core/computed/metrics/largest-contentful-paint-all-frames.js
index 6231927e9..f468e4b34 100644
--- a/lighthouse-core/computed/metrics/largest-contentful-paint-all-frames.js
+++ b/lighthouse-core/computed/metrics/largest-contentful-paint-all-frames.js
@@ -10,10 +10,10 @@
  */
 
 const makeComputedArtifact = require('../computed-artifact.js');
-const ComputedMetric = require('./metric.js');
+const NavigationMetric = require('./navigation-metric.js');
 const LHError = require('../../lib/lh-error.js');
 
-class LargestContentfulPaintAllFrames extends ComputedMetric {
+class LargestContentfulPaintAllFrames extends NavigationMetric {
   /**
    * TODO: Simulate LCP all frames in lantern.
    * @return {Promise<LH.Artifacts.LanternMetric>}
@@ -23,18 +23,18 @@ class LargestContentfulPaintAllFrames extends ComputedMetric {
   }
 
   /**
-   * @param {LH.Artifacts.MetricComputationData} data
+   * @param {LH.Artifacts.NavigationMetricComputationData} data
    * @return {Promise<LH.Artifacts.Metric>}
    */
   static async computeObservedMetric(data) {
-    const {traceOfTab} = data;
-    if (traceOfTab.timings.largestContentfulPaintAllFrames === undefined) {
+    const {processedNavigation} = data;
+    if (processedNavigation.timings.largestContentfulPaintAllFrames === undefined) {
       throw new LHError(LHError.errors.NO_LCP_ALL_FRAMES);
     }
 
     return {
-      timing: traceOfTab.timings.largestContentfulPaintAllFrames,
-      timestamp: traceOfTab.timestamps.largestContentfulPaintAllFrames,
+      timing: processedNavigation.timings.largestContentfulPaintAllFrames,
+      timestamp: processedNavigation.timestamps.largestContentfulPaintAllFrames,
     };
   }
 }
diff --git a/lighthouse-core/computed/metrics/largest-contentful-paint.js b/lighthouse-core/computed/metrics/largest-contentful-paint.js
index a8162ad39..62d14d27d 100644
--- a/lighthouse-core/computed/metrics/largest-contentful-paint.js
+++ b/lighthouse-core/computed/metrics/largest-contentful-paint.js
@@ -14,33 +14,34 @@
  */
 
 const makeComputedArtifact = require('../computed-artifact.js');
-const ComputedMetric = require('./metric.js');
+const NavigationMetric = require('./navigation-metric.js');
 const LHError = require('../../lib/lh-error.js');
 const LanternLargestContentfulPaint = require('./lantern-largest-contentful-paint.js');
 
-class LargestContentfulPaint extends ComputedMetric {
+class LargestContentfulPaint extends NavigationMetric {
   /**
-   * @param {LH.Artifacts.MetricComputationData} data
+   * @param {LH.Artifacts.NavigationMetricComputationData} data
    * @param {LH.Artifacts.ComputedContext} context
    * @return {Promise<LH.Artifacts.LanternMetric>}
    */
   static computeSimulatedMetric(data, context) {
-    return LanternLargestContentfulPaint.request(data, context);
+    const metricData = NavigationMetric.getMetricComputationInput(data);
+    return LanternLargestContentfulPaint.request(metricData, context);
   }
 
   /**
-   * @param {LH.Artifacts.MetricComputationData} data
+   * @param {LH.Artifacts.NavigationMetricComputationData} data
    * @return {Promise<LH.Artifacts.Metric>}
    */
   static async computeObservedMetric(data) {
-    const {traceOfTab} = data;
-    if (traceOfTab.timings.largestContentfulPaint === undefined) {
+    const {processedNavigation} = data;
+    if (processedNavigation.timings.largestContentfulPaint === undefined) {
       throw new LHError(LHError.errors.NO_LCP);
     }
 
     return {
-      timing: traceOfTab.timings.largestContentfulPaint,
-      timestamp: traceOfTab.timestamps.largestContentfulPaint,
+      timing: processedNavigation.timings.largestContentfulPaint,
+      timestamp: processedNavigation.timestamps.largestContentfulPaint,
     };
   }
 }
diff --git a/lighthouse-core/computed/metrics/max-potential-fid.js b/lighthouse-core/computed/metrics/max-potential-fid.js
index efb31af40..4b29c5dfe 100644
--- a/lighthouse-core/computed/metrics/max-potential-fid.js
+++ b/lighthouse-core/computed/metrics/max-potential-fid.js
@@ -6,33 +6,30 @@
 'use strict';
 
 const makeComputedArtifact = require('../computed-artifact.js');
-const MetricArtifact = require('./metric.js');
+const NavigationMetric = require('./navigation-metric.js');
 const LanternMaxPotentialFID = require('./lantern-max-potential-fid.js');
-const LHError = require('../../lib/lh-error.js');
 const TracingProcessor = require('../../lib/tracehouse/trace-processor.js');
 
-class MaxPotentialFID extends MetricArtifact {
+class MaxPotentialFID extends NavigationMetric {
   /**
-   * @param {LH.Artifacts.MetricComputationData} data
+   * @param {LH.Artifacts.NavigationMetricComputationData} data
    * @param {LH.Artifacts.ComputedContext} context
    * @return {Promise<LH.Artifacts.LanternMetric>}
    */
   static computeSimulatedMetric(data, context) {
-    return LanternMaxPotentialFID.request(data, context);
+    const metricData = NavigationMetric.getMetricComputationInput(data);
+    return LanternMaxPotentialFID.request(metricData, context);
   }
 
   /**
-   * @param {LH.Artifacts.MetricComputationData} data
+   * @param {LH.Artifacts.NavigationMetricComputationData} data
    * @return {Promise<LH.Artifacts.Metric>}
    */
   static computeObservedMetric(data) {
-    const {firstContentfulPaint} = data.traceOfTab.timings;
-    if (!firstContentfulPaint) {
-      throw new LHError(LHError.errors.NO_FCP);
-    }
+    const {firstContentfulPaint} = data.processedNavigation.timings;
 
     const events = TracingProcessor.getMainThreadTopLevelEvents(
-      data.traceOfTab,
+      data.processedTrace,
       firstContentfulPaint
     ).filter(evt => evt.duration >= 1);
 
diff --git a/lighthouse-core/computed/metrics/metric.js b/lighthouse-core/computed/metrics/metric.js
index 153663fda..c317a0d06 100644
--- a/lighthouse-core/computed/metrics/metric.js
+++ b/lighthouse-core/computed/metrics/metric.js
@@ -6,7 +6,8 @@
 'use strict';
 
 const TracingProcessor = require('../../lib/tracehouse/trace-processor.js');
-const TraceOfTab = require('../trace-of-tab.js');
+const ProcessedTrace = require('../processed-trace.js');
+const ProcessedNavigation = require('../processed-navigation.js');
 const NetworkRecords = require('../network-records.js');
 
 /**
@@ -18,9 +19,24 @@ const NetworkRecords = require('../network-records.js');
  *     - Override the computeSimulatedMetric method with the simulated-mode implementation (which
  *       may call another computed artifact with the name LanternMyMetricName).
  */
-class ComputedMetric {
+class Metric {
   constructor() {}
 
+  /**
+   * Narrows the metric computation data to the input so child metric requests can be cached.
+   *
+   * @param {LH.Artifacts.MetricComputationData} data
+   * @return {LH.Artifacts.MetricComputationDataInput}
+   */
+  static getMetricComputationInput(data) {
+    return {
+      trace: data.trace,
+      devtoolsLog: data.devtoolsLog,
+      gatherContext: data.gatherContext,
+      settings: data.settings,
+    };
+  }
+
   /**
    * @param {LH.Artifacts.MetricComputationData} data
    * @param {LH.Artifacts.ComputedContext} context
@@ -45,20 +61,31 @@ class ComputedMetric {
    * @return {Promise<LH.Artifacts.LanternMetric|LH.Artifacts.Metric>}
    */
   static async compute_(data, context) {
+    // TODO: remove this fallback when lighthouse-pub-ads plugin can update.
+    const gatherContext = data.gatherContext || {gatherMode: 'navigation'};
     const {trace, devtoolsLog, settings} = data;
     if (!trace || !devtoolsLog || !settings) {
       throw new Error('Did not provide necessary metric computation data');
     }
 
+    const processedTrace = await ProcessedTrace.request(trace, context);
+    const processedNavigation = await ProcessedNavigation.request(processedTrace, context);
+
     const augmentedData = Object.assign({
       networkRecords: await NetworkRecords.request(devtoolsLog, context),
-      traceOfTab: await TraceOfTab.request(trace, context),
+      gatherContext,
+      processedTrace,
+      processedNavigation,
     }, data);
 
-    TracingProcessor.assertHasToplevelEvents(augmentedData.traceOfTab.mainThreadEvents);
+    TracingProcessor.assertHasToplevelEvents(augmentedData.processedTrace.mainThreadEvents);
 
     switch (settings.throttlingMethod) {
       case 'simulate':
+        if (gatherContext.gatherMode !== 'navigation') {
+          throw new Error(`${gatherContext.gatherMode} does not support throttlingMethod simulate`);
+        }
+
         return this.computeSimulatedMetric(augmentedData, context);
       case 'provided':
       case 'devtools':
@@ -69,4 +96,4 @@ class ComputedMetric {
   }
 }
 
-module.exports = ComputedMetric;
+module.exports = Metric;
diff --git a/lighthouse-core/computed/metrics/navigation-metric.js b/lighthouse-core/computed/metrics/navigation-metric.js
new file mode 100644
index 000000000..e87e0ed70
--- /dev/null
+++ b/lighthouse-core/computed/metrics/navigation-metric.js
@@ -0,0 +1,46 @@
+/**
+ * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+'use strict';
+
+const Metric = require('./metric.js');
+
+/**
+ * @fileOverview Enforces that a metric can only be computed on navigations.
+ */
+class NavigationMetric extends Metric {
+  /**
+   * @param {LH.Artifacts.NavigationMetricComputationData} data
+   * @param {LH.Artifacts.ComputedContext} context
+   * @return {Promise<LH.Artifacts.LanternMetric>}
+   */
+  static computeSimulatedMetric(data, context) { // eslint-disable-line no-unused-vars
+    throw new Error('Unimplemented');
+  }
+
+  /**
+   * @param {LH.Artifacts.NavigationMetricComputationData} data
+   * @param {LH.Artifacts.ComputedContext} context
+   * @return {Promise<LH.Artifacts.Metric>}
+   */
+  static computeObservedMetric(data, context) { // eslint-disable-line no-unused-vars
+    throw new Error('Unimplemented');
+  }
+
+  /**
+   * @param {LH.Artifacts.MetricComputationDataInput} data
+   * @param {LH.Artifacts.ComputedContext} context
+   * @return {Promise<LH.Artifacts.LanternMetric|LH.Artifacts.Metric>}
+   */
+  static async compute_(data, context) {
+    if (data.gatherContext.gatherMode !== 'navigation') {
+      throw new Error(`${this.name} can only be computed on navigations`);
+    }
+
+    return super.compute_(data, context);
+  }
+}
+
+module.exports = NavigationMetric;
diff --git a/lighthouse-core/computed/metrics/speed-index.js b/lighthouse-core/computed/metrics/speed-index.js
index 67dc97b1d..ff35ce427 100644
--- a/lighthouse-core/computed/metrics/speed-index.js
+++ b/lighthouse-core/computed/metrics/speed-index.js
@@ -6,22 +6,23 @@
 'use strict';
 
 const makeComputedArtifact = require('../computed-artifact.js');
-const ComputedMetric = require('./metric.js');
+const NavigationMetric = require('./navigation-metric.js');
 const LanternSpeedIndex = require('./lantern-speed-index.js');
 const Speedline = require('../speedline.js');
 
-class SpeedIndex extends ComputedMetric {
+class SpeedIndex extends NavigationMetric {
   /**
-   * @param {LH.Artifacts.MetricComputationData} data
+   * @param {LH.Artifacts.NavigationMetricComputationData} data
    * @param {LH.Artifacts.ComputedContext} context
    * @return {Promise<LH.Artifacts.LanternMetric>}
    */
   static computeSimulatedMetric(data, context) {
-    return LanternSpeedIndex.request(data, context);
+    const metricData = NavigationMetric.getMetricComputationInput(data);
+    return LanternSpeedIndex.request(metricData, context);
   }
 
   /**
-   * @param {LH.Artifacts.MetricComputationData} data
+   * @param {LH.Artifacts.NavigationMetricComputationData} data
    * @param {LH.Artifacts.ComputedContext} context
    * @return {Promise<LH.Artifacts.Metric>}
    */
diff --git a/lighthouse-core/computed/metrics/timing-summary.js b/lighthouse-core/computed/metrics/timing-summary.js
index 99a6eb441..ea017735e 100644
--- a/lighthouse-core/computed/metrics/timing-summary.js
+++ b/lighthouse-core/computed/metrics/timing-summary.js
@@ -5,7 +5,8 @@
  */
 'use strict';
 
-const TraceOfTab = require('../trace-of-tab.js');
+const ProcessedTrace = require('../processed-trace.js');
+const ProcessedNavigation = require('../processed-navigation.js');
 const Speedline = require('../speedline.js');
 const FirstContentfulPaint = require('./first-contentful-paint.js');
 const FirstContentfulPaintAllFrames = require('./first-contentful-paint-all-frames.js');
@@ -23,12 +24,13 @@ class TimingSummary {
   /**
      * @param {LH.Trace} trace
      * @param {LH.DevtoolsLog} devtoolsLog
+     * @param {LH.Artifacts['GatherContext']} gatherContext
      * @param {ImmutableObject<LH.Config.Settings>} settings
      * @param {LH.Artifacts.ComputedContext} context
      * @return {Promise<{metrics: LH.Artifacts.TimingSummary, debugInfo: Record<string,boolean>}>}
      */
-  static async summarize(trace, devtoolsLog, settings, context) {
-    const metricComputationData = {trace, devtoolsLog, settings};
+  static async summarize(trace, devtoolsLog, gatherContext, settings, context) {
+    const metricComputationData = {trace, devtoolsLog, gatherContext, settings};
     /**
      * @template TArtifacts
      * @template TReturn
@@ -40,7 +42,8 @@ class TimingSummary {
       return Artifact.request(artifact, context).catch(_ => undefined);
     };
 
-    const traceOfTab = await TraceOfTab.request(trace, context);
+    const processedTrace = await ProcessedTrace.request(trace, context);
+    const processedNavigation = await ProcessedNavigation.request(processedTrace, context);
     const speedline = await Speedline.request(trace, context);
     const firstContentfulPaint = await FirstContentfulPaint.request(metricComputationData, context);
     const firstContentfulPaintAllFrames = await requestOrUndefined(FirstContentfulPaintAllFrames, metricComputationData); // eslint-disable-line max-len
@@ -83,30 +86,30 @@ class TimingSummary {
       totalCumulativeLayoutShift,
 
       // Include all timestamps of interest from trace of tab
-      observedTimeOrigin: traceOfTab.timings.timeOrigin,
-      observedTimeOriginTs: traceOfTab.timestamps.timeOrigin,
+      observedTimeOrigin: processedTrace.timings.timeOrigin,
+      observedTimeOriginTs: processedTrace.timestamps.timeOrigin,
       // For now, navigationStart is always timeOrigin.
       // These properties might be undefined in a future major version, but preserve them for now.
-      observedNavigationStart: traceOfTab.timings.timeOrigin,
-      observedNavigationStartTs: traceOfTab.timestamps.timeOrigin,
-      observedFirstPaint: traceOfTab.timings.firstPaint,
-      observedFirstPaintTs: traceOfTab.timestamps.firstPaint,
-      observedFirstContentfulPaint: traceOfTab.timings.firstContentfulPaint,
-      observedFirstContentfulPaintTs: traceOfTab.timestamps.firstContentfulPaint,
-      observedFirstContentfulPaintAllFrames: traceOfTab.timings.firstContentfulPaintAllFrames,
-      observedFirstContentfulPaintAllFramesTs: traceOfTab.timestamps.firstContentfulPaintAllFrames,
-      observedFirstMeaningfulPaint: traceOfTab.timings.firstMeaningfulPaint,
-      observedFirstMeaningfulPaintTs: traceOfTab.timestamps.firstMeaningfulPaint,
-      observedLargestContentfulPaint: traceOfTab.timings.largestContentfulPaint,
-      observedLargestContentfulPaintTs: traceOfTab.timestamps.largestContentfulPaint,
-      observedLargestContentfulPaintAllFrames: traceOfTab.timings.largestContentfulPaintAllFrames,
-      observedLargestContentfulPaintAllFramesTs: traceOfTab.timestamps.largestContentfulPaintAllFrames, // eslint-disable-line max-len
-      observedTraceEnd: traceOfTab.timings.traceEnd,
-      observedTraceEndTs: traceOfTab.timestamps.traceEnd,
-      observedLoad: traceOfTab.timings.load,
-      observedLoadTs: traceOfTab.timestamps.load,
-      observedDomContentLoaded: traceOfTab.timings.domContentLoaded,
-      observedDomContentLoadedTs: traceOfTab.timestamps.domContentLoaded,
+      observedNavigationStart: processedTrace.timings.timeOrigin,
+      observedNavigationStartTs: processedTrace.timestamps.timeOrigin,
+      observedFirstPaint: processedNavigation.timings.firstPaint,
+      observedFirstPaintTs: processedNavigation.timestamps.firstPaint,
+      observedFirstContentfulPaint: processedNavigation.timings.firstContentfulPaint,
+      observedFirstContentfulPaintTs: processedNavigation.timestamps.firstContentfulPaint,
+      observedFirstContentfulPaintAllFrames: processedNavigation.timings.firstContentfulPaintAllFrames, // eslint-disable-line max-len
+      observedFirstContentfulPaintAllFramesTs: processedNavigation.timestamps.firstContentfulPaintAllFrames, // eslint-disable-line max-len
+      observedFirstMeaningfulPaint: processedNavigation.timings.firstMeaningfulPaint,
+      observedFirstMeaningfulPaintTs: processedNavigation.timestamps.firstMeaningfulPaint,
+      observedLargestContentfulPaint: processedNavigation.timings.largestContentfulPaint,
+      observedLargestContentfulPaintTs: processedNavigation.timestamps.largestContentfulPaint,
+      observedLargestContentfulPaintAllFrames: processedNavigation.timings.largestContentfulPaintAllFrames, // eslint-disable-line max-len
+      observedLargestContentfulPaintAllFramesTs: processedNavigation.timestamps.largestContentfulPaintAllFrames, // eslint-disable-line max-len
+      observedTraceEnd: processedTrace.timings.traceEnd,
+      observedTraceEndTs: processedTrace.timestamps.traceEnd,
+      observedLoad: processedNavigation.timings.load,
+      observedLoadTs: processedNavigation.timestamps.load,
+      observedDomContentLoaded: processedNavigation.timings.domContentLoaded,
+      observedDomContentLoadedTs: processedNavigation.timestamps.domContentLoaded,
       observedCumulativeLayoutShift: cumulativeLayoutShift,
       observedCumulativeLayoutShiftMainFrame: cumulativeLayoutShiftMainFrame,
       observedTotalCumulativeLayoutShift: totalCumulativeLayoutShift,
@@ -120,17 +123,18 @@ class TimingSummary {
       observedSpeedIndexTs: (speedline.speedIndex + speedline.beginning) * 1000,
     };
     /** @type {Record<string,boolean>} */
-    const debugInfo = {lcpInvalidated: traceOfTab.lcpInvalidated};
+    const debugInfo = {lcpInvalidated: processedNavigation.lcpInvalidated};
 
     return {metrics, debugInfo};
   }
   /**
-   * @param {{trace: LH.Trace, devtoolsLog: LH.DevtoolsLog, settings: ImmutableObject<LH.Config.Settings>}} data
+   * @param {{trace: LH.Trace, devtoolsLog: LH.DevtoolsLog, gatherContext: LH.Artifacts['GatherContext']; settings: ImmutableObject<LH.Config.Settings>}} data
    * @param {LH.Artifacts.ComputedContext} context
    * @return {Promise<{metrics: LH.Artifacts.TimingSummary, debugInfo: Record<string,boolean>}>}
    */
   static async compute_(data, context) {
-    return TimingSummary.summarize(data.trace, data.devtoolsLog, data.settings, context);
+    return TimingSummary.summarize(data.trace, data.devtoolsLog, data.gatherContext, data.settings,
+      context);
   }
 }
 
diff --git a/lighthouse-core/computed/metrics/total-blocking-time.js b/lighthouse-core/computed/metrics/total-blocking-time.js
index 989272478..c9d26cd5b 100644
--- a/lighthouse-core/computed/metrics/total-blocking-time.js
+++ b/lighthouse-core/computed/metrics/total-blocking-time.js
@@ -7,8 +7,7 @@
 
 const makeComputedArtifact = require('../computed-artifact.js');
 const ComputedMetric = require('./metric.js');
-const LHError = require('../../lib/lh-error.js');
-const TracingProcessor = require('../../lib/tracehouse/trace-processor.js');
+const TraceProcessor = require('../../lib/tracehouse/trace-processor.js');
 const LanternTotalBlockingTime = require('./lantern-total-blocking-time.js');
 const TimetoInteractive = require('./interactive.js');
 
@@ -32,14 +31,15 @@ class TotalBlockingTime extends ComputedMetric {
   static get BLOCKING_TIME_THRESHOLD() {
     return 50;
   }
+
   /**
    * @param {Array<{start: number, end: number, duration: number}>} topLevelEvents
-   * @param {number} fcpTimeInMs
-   * @param {number} interactiveTimeMs
+   * @param {number} startTimeMs
+   * @param {number} endTimeMs
    * @return {number}
    */
-  static calculateSumOfBlockingTime(topLevelEvents, fcpTimeInMs, interactiveTimeMs) {
-    if (interactiveTimeMs <= fcpTimeInMs) return 0;
+  static calculateSumOfBlockingTime(topLevelEvents, startTimeMs, endTimeMs) {
+    if (endTimeMs <= startTimeMs) return 0;
 
     const threshold = TotalBlockingTime.BLOCKING_TIME_THRESHOLD;
     let sumBlockingTime = 0;
@@ -47,23 +47,22 @@ class TotalBlockingTime extends ComputedMetric {
       // Early exit for small tasks, which should far outnumber long tasks.
       if (event.duration < threshold) continue;
 
-      // We only want to consider tasks that fall between FCP and TTI.
+      // We only want to consider tasks that fall in our time range (FCP and TTI for navigations).
       // FCP is picked as the lower bound because there is little risk of user input happening
       // before FCP so Long Queuing Qelay regions do not harm user experience. Developers should be
       // optimizing to reach FCP as fast as possible without having to worry about task lengths.
-      if (event.end < fcpTimeInMs) continue;
+      if (event.end < startTimeMs) continue;
 
-      // TTI is picked as the upper bound because we want a well defined end point so that the
-      // metric does not rely on how long we trace.
-      if (event.start > interactiveTimeMs) continue;
+      // TTI is picked as the upper bound because we want a well defined end point for page load.
+      if (event.start > endTimeMs) continue;
 
       // We first perform the clipping, and then calculate Blocking Region. So if we have a 150ms
       // task [0, 150] and FCP happens midway at 50ms, we first clip the task to [50, 150], and then
       // calculate the Blocking Region to be [100, 150]. The rational here is that tasks before FCP
       // are unimportant, so we care whether the main thread is busy more than 50ms at a time only
       // after FCP.
-      const clippedStart = Math.max(event.start, fcpTimeInMs);
-      const clippedEnd = Math.min(event.end, interactiveTimeMs);
+      const clippedStart = Math.max(event.start, startTimeMs);
+      const clippedEnd = Math.min(event.end, endTimeMs);
       const clippedDuration = clippedEnd - clippedStart;
       if (clippedDuration < threshold) continue;
 
@@ -71,7 +70,7 @@ class TotalBlockingTime extends ComputedMetric {
       // Example:
       //   [              250ms Task                   ]
       //   | First 50ms |   Blocking Region (200ms)    |
-      sumBlockingTime += (clippedDuration - threshold);
+      sumBlockingTime += clippedDuration - threshold;
     }
 
     return sumBlockingTime;
@@ -83,7 +82,8 @@ class TotalBlockingTime extends ComputedMetric {
    * @return {Promise<LH.Artifacts.LanternMetric>}
    */
   static computeSimulatedMetric(data, context) {
-    return LanternTotalBlockingTime.request(data, context);
+    const metricData = ComputedMetric.getMetricComputationInput(data);
+    return LanternTotalBlockingTime.request(metricData, context);
   }
 
   /**
@@ -92,22 +92,29 @@ class TotalBlockingTime extends ComputedMetric {
    * @return {Promise<LH.Artifacts.Metric>}
    */
   static async computeObservedMetric(data, context) {
-    const {firstContentfulPaint} = data.traceOfTab.timings;
-    if (!firstContentfulPaint) {
-      throw new LHError(LHError.errors.NO_FCP);
-    }
+    const events = TraceProcessor.getMainThreadTopLevelEvents(data.processedTrace);
 
-    const interactiveTimeMs = (await TimetoInteractive.request(data, context)).timing;
+    if (data.processedNavigation) {
+      const {firstContentfulPaint} = data.processedNavigation.timings;
+      const metricData = ComputedMetric.getMetricComputationInput(data);
+      const interactiveTimeMs = (await TimetoInteractive.request(metricData, context)).timing;
 
-    const events = TracingProcessor.getMainThreadTopLevelEvents(data.traceOfTab);
-
-    return {
-      timing: TotalBlockingTime.calculateSumOfBlockingTime(
-        events,
-        firstContentfulPaint,
-        interactiveTimeMs
-      ),
-    };
+      return {
+        timing: TotalBlockingTime.calculateSumOfBlockingTime(
+          events,
+          firstContentfulPaint,
+          interactiveTimeMs
+        ),
+      };
+    } else {
+      return {
+        timing: TotalBlockingTime.calculateSumOfBlockingTime(
+          events,
+          0,
+          data.processedTrace.timestamps.traceEnd
+        ),
+      };
+    }
   }
 }
 
diff --git a/lighthouse-core/computed/page-dependency-graph.js b/lighthouse-core/computed/page-dependency-graph.js
index 74dffc38f..513329a8c 100644
--- a/lighthouse-core/computed/page-dependency-graph.js
+++ b/lighthouse-core/computed/page-dependency-graph.js
@@ -11,7 +11,7 @@ const CPUNode = require('../lib/dependency-graph/cpu-node.js');
 const NetworkAnalyzer = require('../lib/dependency-graph/simulator/network-analyzer.js');
 const TracingProcessor = require('../lib/tracehouse/trace-processor.js');
 const NetworkRequest = require('../lib/network-request.js');
-const TraceOfTab = require('./trace-of-tab.js');
+const ProcessedTrace = require('./processed-trace.js');
 const NetworkRecords = require('./network-records.js');
 
 /** @typedef {import('../lib/dependency-graph/base-node.js').Node} Node */
@@ -102,18 +102,18 @@ class PageDependencyGraph {
   }
 
   /**
-   * @param {LH.Artifacts.TraceOfTab} traceOfTab
+   * @param {LH.Artifacts.ProcessedTrace} processedTrace
    * @return {Array<CPUNode>}
    */
-  static getCPUNodes(traceOfTab) {
+  static getCPUNodes({mainThreadEvents}) {
     /** @type {Array<CPUNode>} */
     const nodes = [];
     let i = 0;
 
-    TracingProcessor.assertHasToplevelEvents(traceOfTab.mainThreadEvents);
+    TracingProcessor.assertHasToplevelEvents(mainThreadEvents);
 
-    while (i < traceOfTab.mainThreadEvents.length) {
-      const evt = traceOfTab.mainThreadEvents[i];
+    while (i < mainThreadEvents.length) {
+      const evt = mainThreadEvents[i];
       i++;
 
       // Skip all trace events that aren't schedulable tasks with sizable duration
@@ -126,10 +126,10 @@ class PageDependencyGraph {
       const children = [];
       for (
         const endTime = evt.ts + evt.dur;
-        i < traceOfTab.mainThreadEvents.length && traceOfTab.mainThreadEvents[i].ts < endTime;
+        i < mainThreadEvents.length && mainThreadEvents[i].ts < endTime;
         i++
       ) {
-        children.push(traceOfTab.mainThreadEvents[i]);
+        children.push(mainThreadEvents[i]);
       }
 
       nodes.push(new CPUNode(evt, children));
@@ -380,13 +380,13 @@ class PageDependencyGraph {
   }
 
   /**
-   * @param {LH.Artifacts.TraceOfTab} traceOfTab
+   * @param {LH.Artifacts.ProcessedTrace} processedTrace
    * @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
    * @return {Node}
    */
-  static createGraph(traceOfTab, networkRecords) {
+  static createGraph(processedTrace, networkRecords) {
     const networkNodeOutput = PageDependencyGraph.getNetworkNodeOutput(networkRecords);
-    const cpuNodes = PageDependencyGraph.getCPUNodes(traceOfTab);
+    const cpuNodes = PageDependencyGraph.getCPUNodes(processedTrace);
 
     // The root request is the earliest network request, using position in networkRecords array to break ties.
     const rootRequest = networkRecords.reduce((min, r) => (r.startTime < min.startTime ? r : min));
@@ -458,12 +458,12 @@ class PageDependencyGraph {
   static async compute_(data, context) {
     const trace = data.trace;
     const devtoolsLog = data.devtoolsLog;
-    const [traceOfTab, networkRecords] = await Promise.all([
-      TraceOfTab.request(trace, context),
+    const [processedTrace, networkRecords] = await Promise.all([
+      ProcessedTrace.request(trace, context),
       NetworkRecords.request(devtoolsLog, context),
     ]);
 
-    return PageDependencyGraph.createGraph(traceOfTab, networkRecords);
+    return PageDependencyGraph.createGraph(processedTrace, networkRecords);
   }
 }
 
diff --git a/lighthouse-core/computed/processed-navigation.js b/lighthouse-core/computed/processed-navigation.js
new file mode 100644
index 000000000..47893afb8
--- /dev/null
+++ b/lighthouse-core/computed/processed-navigation.js
@@ -0,0 +1,21 @@
+/**
+ * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+'use strict';
+
+const makeComputedArtifact = require('./computed-artifact.js');
+const LHTraceProcessor = require('../lib/lh-trace-processor.js');
+
+class ProcessedNavigation {
+  /**
+     * @param {LH.Artifacts.ProcessedTrace} processedTrace
+     * @return {Promise<LH.Artifacts.ProcessedNavigation>}
+    */
+  static async compute_(processedTrace) {
+    return LHTraceProcessor.processNavigation(processedTrace);
+  }
+}
+
+module.exports = makeComputedArtifact(ProcessedNavigation);
diff --git a/lighthouse-core/computed/processed-trace.js b/lighthouse-core/computed/processed-trace.js
new file mode 100644
index 000000000..d83752d34
--- /dev/null
+++ b/lighthouse-core/computed/processed-trace.js
@@ -0,0 +1,21 @@
+/**
+ * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+'use strict';
+
+const makeComputedArtifact = require('./computed-artifact.js');
+const LHTraceProcessor = require('../lib/lh-trace-processor.js');
+
+class ProcessedTrace {
+  /**
+    * @param {LH.Trace} trace
+    * @return {Promise<LH.Artifacts.ProcessedTrace>}
+   */
+  static async compute_(trace) {
+    return LHTraceProcessor.processTrace(trace);
+  }
+}
+
+module.exports = makeComputedArtifact(ProcessedTrace);
diff --git a/lighthouse-core/computed/speedline.js b/lighthouse-core/computed/speedline.js
index 48c74ae53..6422d32be 100644
--- a/lighthouse-core/computed/speedline.js
+++ b/lighthouse-core/computed/speedline.js
@@ -8,7 +8,7 @@
 const makeComputedArtifact = require('./computed-artifact.js');
 const speedline = require('speedline-core');
 const LHError = require('../lib/lh-error.js');
-const TraceOfTab = require('./trace-of-tab.js');
+const ProcessedTrace = require('./processed-trace.js');
 
 class Speedline {
   /**
@@ -19,13 +19,13 @@ class Speedline {
   static async compute_(trace, context) {
     // speedline() may throw without a promise, so we resolve immediately
     // to get in a promise chain.
-    return TraceOfTab.request(trace, context).then(traceOfTab => {
+    return ProcessedTrace.request(trace, context).then(processedTrace => {
       // Use a shallow copy of traceEvents so speedline can sort as it pleases.
       // See https://github.com/GoogleChrome/lighthouse/issues/2333
       const traceEvents = trace.traceEvents.slice();
       // Force use of timeOrigin as reference point for speedline
       // See https://github.com/GoogleChrome/lighthouse/issues/2095
-      const timeOrigin = traceOfTab.timestamps.timeOrigin;
+      const timeOrigin = processedTrace.timestamps.timeOrigin;
       return speedline(traceEvents, {
         timeOrigin,
         fastMode: true,
diff --git a/lighthouse-core/computed/trace-of-tab.js b/lighthouse-core/computed/trace-of-tab.js
index 4f30851c1..71f0a70da 100644
--- a/lighthouse-core/computed/trace-of-tab.js
+++ b/lighthouse-core/computed/trace-of-tab.js
@@ -5,97 +5,26 @@
  */
 'use strict';
 
-const makeComputedArtifact = require('./computed-artifact.js');
-const LHError = require('../lib/lh-error.js');
-const TraceProcessor = require('../lib/tracehouse/trace-processor.js');
-
-// TraceProcessor throws generic errors, but we'd like our special localized and code-specific LHError
-// objects to be thrown instead.
-class LHTraceProcessor extends TraceProcessor {
-  /**
-   * @return {Error}
-   */
-  static createNoNavstartError() {
-    return new LHError(LHError.errors.NO_NAVSTART);
-  }
-
-  /**
-   * This isn't currently used, but will be when the time origin of trace processing is changed.
-   * @see {TraceProcessor.computeTimeOrigin}
-   * @see https://github.com/GoogleChrome/lighthouse/pull/11253#discussion_r507985527
-   * @return {Error}
-   */
-  static createNoResourceSendRequestError() {
-    return new LHError(LHError.errors.NO_RESOURCE_REQUEST);
-  }
-
-  /**
-   * @return {Error}
-   */
-  static createNoTracingStartedError() {
-    return new LHError(LHError.errors.NO_TRACING_STARTED);
-  }
-}
+/** @fileoverview This file is no longer used internally, but remains here for backcompat with plugins. */
 
+const log = require('lighthouse-logger');
+const makeComputedArtifact = require('./computed-artifact.js');
+const ProcessedTrace = require('./processed-trace.js');
+const ProcessedNavigation = require('./processed-navigation.js');
 
 class TraceOfTab {
   /**
-   * Finds key trace events, identifies main process/thread, and returns timings of trace events
-   * in milliseconds since navigation start in addition to the standard microsecond monotonic timestamps.
-   * @param {LH.Trace} trace
-   * @return {Promise<LH.Artifacts.TraceOfTab>}
-  */
-  static async compute_(trace) {
-    // Trace of tab doesn't require FCP to exist, but all of LH requires it.
-    // We'll check that we got an FCP here and re-type accordingly so all of our consumers don't
-    // have to repeat this check.
-    const traceOfTab = await LHTraceProcessor.computeTraceOfTab(trace);
-    const {
-      timings,
-      timestamps,
-      firstContentfulPaintEvt,
-      firstContentfulPaintAllFramesEvt,
-    } = traceOfTab;
-    const {
-      firstContentfulPaint: firstContentfulPaintTiming,
-      firstContentfulPaintAllFrames: firstContentfulPaintAllFramesTiming,
-    } = timings;
-    const {
-      firstContentfulPaint: firstContentfulPaintTs,
-      firstContentfulPaintAllFrames: firstContentfulPaintAllFramesTs,
-    } = timestamps;
-
-    if (
-      !firstContentfulPaintEvt ||
-      firstContentfulPaintTiming === undefined ||
-      firstContentfulPaintTs === undefined ||
-      // FCP-AF will only be undefined if FCP is also undefined.
-      // These conditions are for enforcing types and should never actually trigger.
-      !firstContentfulPaintAllFramesEvt ||
-      firstContentfulPaintAllFramesTiming === undefined ||
-      firstContentfulPaintAllFramesTs === undefined
-    ) {
-      throw new LHError(LHError.errors.NO_FCP);
-    }
-
-    // We already know that `traceOfTab` is good to go at this point, but tsc doesn't yet.
-    // Help tsc out by reconstructing the object manually with the known defined values.
-    return {
-      ...traceOfTab,
-      firstContentfulPaintEvt,
-      firstContentfulPaintAllFramesEvt,
-      timings: {
-        ...timings,
-        firstContentfulPaint: firstContentfulPaintTiming,
-        firstContentfulPaintAllFrames: firstContentfulPaintAllFramesTiming,
-      },
-      timestamps: {
-        ...timestamps,
-        firstContentfulPaint: firstContentfulPaintTs,
-        firstContentfulPaintAllFrames: firstContentfulPaintAllFramesTs,
-      },
-    };
+     * @param {LH.Trace} trace
+     * @param {LH.Artifacts.ComputedContext} context
+     * @return {Promise<any>}
+    */
+  static async compute_(trace, context) {
+    const processedTrace = await ProcessedTrace.request(trace, context);
+    const processedNavigation = await ProcessedNavigation.request(processedTrace, context);
+    return {...processedTrace, ...processedNavigation};
   }
 }
 
+log.warn(`trace-of-tab`, `trace-of-tab is deprecated, use processed-trace / processed-navigation instead`); // eslint-disable-line max-len
 module.exports = makeComputedArtifact(TraceOfTab);
+
diff --git a/lighthouse-core/computed/user-timings.js b/lighthouse-core/computed/user-timings.js
index 7ad702ea3..e06386bab 100644
--- a/lighthouse-core/computed/user-timings.js
+++ b/lighthouse-core/computed/user-timings.js
@@ -6,7 +6,7 @@
 'use strict';
 
 const makeComputedArtifact = require('./computed-artifact.js');
-const TraceOfTab = require('./trace-of-tab.js');
+const ProcessedTrace = require('./processed-trace.js');
 
 /** @typedef {{name: string, isMark: true, args: LH.TraceEvent['args'], startTime: number}} MarkEvent */
 /** @typedef {{name: string, isMark: false, args: LH.TraceEvent['args'], startTime: number, endTime: number, duration: number}} MeasureEvent */
@@ -18,7 +18,7 @@ class UserTimings {
    * @return {Promise<Array<MarkEvent|MeasureEvent>>}
    */
   static async compute_(trace, context) {
-    const traceOfTab = await TraceOfTab.request(trace, context);
+    const processedTrace = await ProcessedTrace.request(trace, context);
     /** @type {Array<MarkEvent|MeasureEvent>} */
     const userTimings = [];
     /** @type {Record<string, number>} */
@@ -28,7 +28,7 @@ class UserTimings {
     // The event phases we are interested in are mark and instant events (R, i, I)
     // and duration events which correspond to measures (B, b, E, e).
     // @see https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview#
-    traceOfTab.processEvents.filter(evt => {
+    processedTrace.processEvents.filter(evt => {
       if (!evt.cat.includes('blink.user_timing')) {
         return false;
       }
@@ -69,9 +69,9 @@ class UserTimings {
 
     // baseline the timestamps against the timeOrigin, and translate to milliseconds
     userTimings.forEach(ut => {
-      ut.startTime = (ut.startTime - traceOfTab.timeOriginEvt.ts) / 1000;
+      ut.startTime = (ut.startTime - processedTrace.timeOriginEvt.ts) / 1000;
       if (!ut.isMark) {
-        ut.endTime = (ut.endTime - traceOfTab.timeOriginEvt.ts) / 1000;
+        ut.endTime = (ut.endTime - processedTrace.timeOriginEvt.ts) / 1000;
         ut.duration = ut.duration / 1000;
       }
     });
diff --git a/lighthouse-core/config/default-config.js b/lighthouse-core/config/default-config.js
index 60c2522d8..b8ffa1d31 100644
--- a/lighthouse-core/config/default-config.js
+++ b/lighthouse-core/config/default-config.js
@@ -146,6 +146,7 @@ const defaultConfig = {
       'iframe-elements',
       'form-elements',
       'main-document-content',
+      'gather-context',
       'global-listeners',
       'dobetterweb/appcache',
       'dobetterweb/doctype',
diff --git a/lighthouse-core/fraggle-rock/config/default-config.js b/lighthouse-core/fraggle-rock/config/default-config.js
index c5a601593..e724a8abe 100644
--- a/lighthouse-core/fraggle-rock/config/default-config.js
+++ b/lighthouse-core/fraggle-rock/config/default-config.js
@@ -24,6 +24,7 @@ const artifacts = {
   FontSize: '',
   FormElements: '',
   FullPageScreenshot: '',
+  GatherContext: '',
   GlobalListeners: '',
   HostFormFactor: '',
   HostUserAgent: '',
@@ -78,6 +79,7 @@ const defaultConfig = {
     {id: artifacts.FontSize, gatherer: 'seo/font-size'},
     {id: artifacts.FormElements, gatherer: 'form-elements'},
     {id: artifacts.FullPageScreenshot, gatherer: 'full-page-screenshot'},
+    {id: artifacts.GatherContext, gatherer: 'gather-context'},
     {id: artifacts.GlobalListeners, gatherer: 'global-listeners'},
     {id: artifacts.IFrameElements, gatherer: 'iframe-elements'},
     {id: artifacts.ImageElements, gatherer: 'image-elements'},
@@ -132,6 +134,7 @@ const defaultConfig = {
         artifacts.FontSize,
         artifacts.FormElements,
         artifacts.FullPageScreenshot,
+        artifacts.GatherContext,
         artifacts.GlobalListeners,
         artifacts.IFrameElements,
         artifacts.ImageElements,
diff --git a/lighthouse-core/fraggle-rock/config/filters.js b/lighthouse-core/fraggle-rock/config/filters.js
index 40d9c1b9d..c65cb419c 100644
--- a/lighthouse-core/fraggle-rock/config/filters.js
+++ b/lighthouse-core/fraggle-rock/config/filters.js
@@ -44,12 +44,29 @@ function filterArtifactsByGatherMode(artifacts, mode) {
 function filterAuditsByAvailableArtifacts(audits, availableArtifacts) {
   if (!audits) return null;
 
-  const availableAritfactIds = new Set(
+  const availableArtifactIds = new Set(
     availableArtifacts.map(artifact => artifact.id).concat(baseArtifactKeys)
   );
-  return audits.filter(audit =>
-    audit.implementation.meta.requiredArtifacts.every(id => availableAritfactIds.has(id))
-  );
+  return audits.filter(audit => {
+    const meta = audit.implementation.meta;
+    return meta.requiredArtifacts.every(id => availableArtifactIds.has(id));
+  });
+}
+
+/**
+ * Optional `supportedModes` property can explicitly exclude an audit even if all required artifacts are available.
+ *
+ * @param {LH.Config.FRConfig['audits']} audits
+ * @param {LH.Gatherer.GatherMode} mode
+ * @return {LH.Config.FRConfig['audits']}
+ */
+function filterAuditsByGatherMode(audits, mode) {
+  if (!audits) return null;
+
+  return audits.filter(audit => {
+    const meta = audit.implementation.meta;
+    return !meta.supportedModes || meta.supportedModes.includes(mode);
+  });
 }
 
 /**
@@ -100,7 +117,8 @@ function filterCategoriesByAvailableAudits(categories, availableAudits) {
  */
 function filterConfigByGatherMode(config, mode) {
   const artifacts = filterArtifactsByGatherMode(config.artifacts, mode);
-  const audits = filterAuditsByAvailableArtifacts(config.audits, artifacts || []);
+  const availableAudits = filterAuditsByAvailableArtifacts(config.audits, artifacts || []);
+  const audits = filterAuditsByGatherMode(availableAudits, mode);
   const categories = filterCategoriesByAvailableAudits(config.categories, audits || []);
 
   return {
@@ -115,5 +133,6 @@ module.exports = {
   filterConfigByGatherMode,
   filterArtifactsByGatherMode,
   filterAuditsByAvailableArtifacts,
+  filterAuditsByGatherMode,
   filterCategoriesByAvailableAudits,
 };
diff --git a/lighthouse-core/gather/gatherers/gather-context.js b/lighthouse-core/gather/gatherers/gather-context.js
new file mode 100644
index 000000000..6b1c3bdd7
--- /dev/null
+++ b/lighthouse-core/gather/gatherers/gather-context.js
@@ -0,0 +1,25 @@
+/**
+ * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+'use strict';
+
+const FRGatherer = require('../../fraggle-rock/gather/base-gatherer.js');
+
+class GatherContext extends FRGatherer {
+   /** @type {LH.Gatherer.GathererMeta} */
+   meta = {
+     supportedModes: ['snapshot', 'timespan', 'navigation'],
+   }
+
+   /**
+    * @param {LH.Gatherer.FRTransitionalContext} phaseContext
+    * @return {Promise<LH.Artifacts['GatherContext']>}
+    */
+   async getArtifact(phaseContext) {
+     return {gatherMode: phaseContext.gatherMode};
+   }
+}
+
+module.exports = GatherContext;
diff --git a/lighthouse-core/gather/gatherers/seo/tap-targets.js b/lighthouse-core/gather/gatherers/seo/tap-targets.js
index 93e37fc9d..e7511a69c 100644
--- a/lighthouse-core/gather/gatherers/seo/tap-targets.js
+++ b/lighthouse-core/gather/gatherers/seo/tap-targets.js
@@ -166,15 +166,11 @@ function elementCenterIsAtZAxisTop(el, elCenterPoint) {
 /**
  * Finds all position sticky/absolute elements on the page and adds a class
  * that disables pointer events on them.
+ * @param {string} className
  * @return {() => void} - undo function to re-enable pointer events
  */
 /* c8 ignore start */
-function disableFixedAndStickyElementPointerEvents() {
-  const className = 'lighthouse-disable-pointer-events';
-  const styleTag = document.createElement('style');
-  styleTag.textContent = `.${className} { pointer-events: none !important }`;
-  document.body.appendChild(styleTag);
-
+function disableFixedAndStickyElementPointerEvents(className) {
   document.querySelectorAll('*').forEach(el => {
     const position = getComputedStyle(el).position;
     if (position === 'fixed' || position === 'sticky') {
@@ -186,17 +182,17 @@ function disableFixedAndStickyElementPointerEvents() {
     Array.from(document.getElementsByClassName(className)).forEach(el => {
       el.classList.remove(className);
     });
-    styleTag.remove();
   };
 }
 /* c8 ignore stop */
 
 /**
  * @param {string} tapTargetsSelector
+ * @param {string} className
  * @return {LH.Artifacts.TapTarget[]}
  */
 /* c8 ignore start */
-function gatherTapTargets(tapTargetsSelector) {
+function gatherTapTargets(tapTargetsSelector, className) {
   /** @type {LH.Artifacts.TapTarget[]} */
   const targets = [];
 
@@ -237,7 +233,8 @@ function gatherTapTargets(tapTargetsSelector) {
   // Disable pointer events so that tap targets below them don't get
   // detected as non-tappable (they are tappable, just not while the viewport
   // is at the current scroll position)
-  const reenableFixedAndStickyElementPointerEvents = disableFixedAndStickyElementPointerEvents();
+  const reenableFixedAndStickyElementPointerEvents =
+    disableFixedAndStickyElementPointerEvents(className);
 
   /** @type {{
     tapTargetElement: Element,
@@ -288,13 +285,49 @@ function gatherTapTargets(tapTargetsSelector) {
 /* c8 ignore stop */
 
 class TapTargets extends FRGatherer {
+  /**
+   * @param {LH.Gatherer.FRProtocolSession} session
+   * @param {string} className
+   * @return {Promise<string>}
+   */
+  async addStyleRule(session, className) {
+    const frameTreeResponse = await session.sendCommand('Page.getFrameTree');
+    const {styleSheetId} = await session.sendCommand('CSS.createStyleSheet', {
+      frameId: frameTreeResponse.frameTree.frame.id,
+    });
+    const ruleText = `.${className} { pointer-events: none !important }`;
+    await session.sendCommand('CSS.setStyleSheetText', {
+      styleSheetId,
+      text: ruleText,
+    });
+    return styleSheetId;
+  }
+
+  /**
+   * @param {LH.Gatherer.FRProtocolSession} session
+   * @param {string} styleSheetId
+   */
+  async removeStyleRule(session, styleSheetId) {
+    await session.sendCommand('CSS.setStyleSheetText', {
+      styleSheetId,
+      text: '',
+    });
+  }
+
   /**
    * @param {LH.Gatherer.FRTransitionalContext} passContext
    * @return {Promise<LH.Artifacts.TapTarget[]>} All visible tap targets with their positions and sizes
    */
-  getArtifact(passContext) {
-    return passContext.driver.executionContext.evaluate(gatherTapTargets, {
-      args: [tapTargetsSelector],
+  async getArtifact(passContext) {
+    const session = passContext.driver.defaultSession;
+    await session.sendCommand('DOM.enable');
+    await session.sendCommand('CSS.enable');
+
+    const className = 'lighthouse-disable-pointer-events';
+    const styleSheetId = await this.addStyleRule(session, className);
+
+    const tapTargets = await passContext.driver.executionContext.evaluate(gatherTapTargets, {
+      args: [tapTargetsSelector, className],
       useIsolation: true,
       deps: [
         pageFunctions.getNodeDetailsString,
@@ -312,6 +345,13 @@ class TapTargets extends FRGatherer {
         pageFunctions.getNodeLabel,
       ],
     });
+
+    await this.removeStyleRule(session, styleSheetId);
+
+    await session.sendCommand('CSS.disable');
+    await session.sendCommand('DOM.disable');
+
+    return tapTargets;
   }
 }
 
diff --git a/lighthouse-core/gather/gatherers/trace-elements.js b/lighthouse-core/gather/gatherers/trace-elements.js
index bd8ca6d4f..fd5de00f1 100644
--- a/lighthouse-core/gather/gatherers/trace-elements.js
+++ b/lighthouse-core/gather/gatherers/trace-elements.js
@@ -16,10 +16,11 @@
 const FRGatherer = require('../../fraggle-rock/gather/base-gatherer.js');
 const {resolveNodeIdToObjectId} = require('../driver/dom.js');
 const pageFunctions = require('../../lib/page-functions.js');
-const TraceProcessor = require('../../lib/tracehouse/trace-processor.js');
 const RectHelpers = require('../../lib/rect-helpers.js');
 const Sentry = require('../../lib/sentry.js');
 const Trace = require('./trace.js');
+const ProcessedTrace = require('../../computed/processed-trace.js');
+const ProcessedNavigation = require('../../computed/processed-navigation.js');
 
 /** @typedef {{nodeId: number, score?: number, animations?: {name?: string, failureReasonsMask?: number, unsupportedProperties?: string[]}[]}} TraceElementData */
 
@@ -255,8 +256,9 @@ class TraceElements extends FRGatherer {
       throw new Error('Trace is missing!');
     }
 
-    const {largestContentfulPaintEvt, mainThreadEvents} =
-      TraceProcessor.computeTraceOfTab(trace);
+    const processedTrace = await ProcessedTrace.request(trace, context);
+    const {largestContentfulPaintEvt} = await ProcessedNavigation.request(processedTrace, context);
+    const {mainThreadEvents} = processedTrace;
 
     const lcpNodeId = TraceElements.getNodeIDFromTraceEvent(largestContentfulPaintEvt);
     const clsNodeData = TraceElements.getTopLayoutShiftElements(mainThreadEvents);
diff --git a/lighthouse-core/lib/lh-trace-processor.js b/lighthouse-core/lib/lh-trace-processor.js
new file mode 100644
index 000000000..53c69cc4f
--- /dev/null
+++ b/lighthouse-core/lib/lh-trace-processor.js
@@ -0,0 +1,46 @@
+/**
+ * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+'use strict';
+
+const LHError = require('../lib/lh-error.js');
+const TraceProcessor = require('../lib/tracehouse/trace-processor.js');
+
+// TraceProcessor throws generic errors, but we'd like our special localized and code-specific LHError
+// objects to be thrown instead.
+class LHTraceProcessor extends TraceProcessor {
+  /**
+   * @return {Error}
+   */
+  static createNoNavstartError() {
+    return new LHError(LHError.errors.NO_NAVSTART);
+  }
+
+  /**
+   * This isn't currently used, but will be when the time origin of trace processing is changed.
+   * @see {TraceProcessor.computeTimeOrigin}
+   * @see https://github.com/GoogleChrome/lighthouse/pull/11253#discussion_r507985527
+   * @return {Error}
+   */
+  static createNoResourceSendRequestError() {
+    return new LHError(LHError.errors.NO_RESOURCE_REQUEST);
+  }
+
+  /**
+   * @return {Error}
+   */
+  static createNoTracingStartedError() {
+    return new LHError(LHError.errors.NO_TRACING_STARTED);
+  }
+
+  /**
+   * @return {Error}
+   */
+  static createNoFirstContentfulPaintError() {
+    return new LHError(LHError.errors.NO_FCP);
+  }
+}
+
+module.exports = LHTraceProcessor;
diff --git a/lighthouse-core/lib/tracehouse/trace-processor.js b/lighthouse-core/lib/tracehouse/trace-processor.js
index 393d651d6..50b26e8f0 100644
--- a/lighthouse-core/lib/tracehouse/trace-processor.js
+++ b/lighthouse-core/lib/tracehouse/trace-processor.js
@@ -16,9 +16,7 @@
  * 4. Return all those items in one handy bundle.
  */
 
-/** @typedef {Omit<LH.Artifacts.TraceTimes, 'firstContentfulPaint'|'firstContentfulPaintAllFrames'> & {firstContentfulPaint?: number, firstContentfulPaintAllFrames?: number}} TraceTimesWithoutFCP */
-/** @typedef {Omit<TraceTimesWithoutFCP, 'traceEnd'>} TraceTimesWithoutFCPAndTraceEnd */
-/** @typedef {Omit<LH.Artifacts.TraceOfTab, 'firstContentfulPaintEvt'|'firstContentfulPaintAllFramesEvt'|'timings'|'timestamps'> & {timings: TraceTimesWithoutFCP, timestamps: TraceTimesWithoutFCP, firstContentfulPaintEvt?: LH.Artifacts.TraceOfTab['firstContentfulPaintEvt'], firstContentfulPaintAllFramesEvt?: LH.Artifacts.TraceOfTab['largestContentfulPaintAllFramesEvt']}} TraceOfTabWithoutFCP */
+/** @typedef {Omit<LH.Artifacts.NavigationTraceTimes, 'firstContentfulPaintAllFrames'|'traceEnd'>} TraceNavigationTimesForFrame */
 /** @typedef {'lastNavigationStart'|'firstResourceSendRequest'} TimeOriginDeterminationMethod */
 /** @typedef {Omit<LH.TraceEvent, 'name'|'args'> & {name: 'FrameCommittedInBrowser', args: {data: {frame: string, url: string, parent?: string}}}} FrameCommittedEvent */
 /** @typedef {Omit<LH.TraceEvent, 'name'|'args'> & {name: 'largestContentfulPaint::Invalidate'|'largestContentfulPaint::Candidate', args: {data?: {size?: number}, frame: string}}} LCPEvent */
@@ -62,6 +60,13 @@ class TraceProcessor {
     return new Error('No tracingStartedInBrowser event found');
   }
 
+  /**
+   * @return {Error}
+   */
+  static createNoFirstContentfulPaintError() {
+    return new Error('No FirstContentfulPaint event found');
+  }
+
   /**
    * Returns true if the event is a navigation start event of a document whose URL seems valid.
    *
@@ -375,21 +380,21 @@ class TraceProcessor {
   }
 
   /**
-   * Provides the top level events on the main thread with timestamps in ms relative to navigation
+   * Provides the top level events on the main thread with timestamps in ms relative to timeOrigin.
    * start.
-   * @param {LH.Artifacts.TraceOfTab} tabTrace
+   * @param {LH.Artifacts.ProcessedTrace} trace
    * @param {number=} startTime Optional start time (in ms relative to timeOrigin) of range of interest. Defaults to 0.
    * @param {number=} endTime Optional end time (in ms relative to timeOrigin) of range of interest. Defaults to trace end.
    * @return {Array<ToplevelEvent>}
    */
-  static getMainThreadTopLevelEvents(tabTrace, startTime = 0, endTime = Infinity) {
+  static getMainThreadTopLevelEvents(trace, startTime = 0, endTime = Infinity) {
     const topLevelEvents = [];
     // note: mainThreadEvents is already sorted by event start
-    for (const event of tabTrace.mainThreadEvents) {
+    for (const event of trace.mainThreadEvents) {
       if (!this.isScheduleableTask(event) || !event.dur) continue;
 
-      const start = (event.ts - tabTrace.timeOriginEvt.ts) / 1000;
-      const end = (event.ts + event.dur - tabTrace.timeOriginEvt.ts) / 1000;
+      const start = (event.ts - trace.timeOriginEvt.ts) / 1000;
+      const end = (event.ts + event.dur - trace.timeOriginEvt.ts) / 1000;
       if (start > endTime || end < startTime) continue;
 
       topLevelEvents.push({
@@ -576,9 +581,9 @@ class TraceProcessor {
    * in milliseconds since the time origin in addition to the standard microsecond monotonic timestamps.
    * @param {LH.Trace} trace
    * @param {{timeOriginDeterminationMethod?: TimeOriginDeterminationMethod}} [options]
-   * @return {TraceOfTabWithoutFCP}
+   * @return {LH.Artifacts.ProcessedTrace}
   */
-  static computeTraceOfTab(trace, options) {
+  static processTrace(trace, options) {
     const {timeOriginDeterminationMethod = 'lastNavigationStart'} = options || {};
 
     // Parse the trace for our key events and sort them by timestamp. Note: sort
@@ -638,17 +643,6 @@ class TraceProcessor {
       timeOriginDeterminationMethod
     );
 
-    // Compute the key frame timings for the main frame.
-    const frameTimings = this.computeKeyTimingsForFrame(frameEvents, {timeOriginEvt});
-
-    // Compute FCP for all frames.
-    const fcpAllFramesEvt = frameTreeEvents.find(
-      e => e.name === 'firstContentfulPaint' && e.ts > timeOriginEvt.ts
-    );
-
-    // Compute LCP for all frames.
-    const lcpAllFramesEvt = this.computeValidLCPAllFrames(frameTreeEvents, timeOriginEvt).lcp;
-
     // Subset all trace events to just our tab's process (incl threads other than main)
     // stable-sort events to keep them correctly nested.
     const processEvents = TraceProcessor
@@ -660,41 +654,81 @@ class TraceProcessor {
     // Ensure our traceEnd reflects all page activity.
     const traceEnd = this.computeTraceEnd(trace.traceEvents, timeOriginEvt);
 
-    /** @param {number|undefined} ts */
-    const maybeGetTiming = (ts) => ts === undefined ? undefined : (ts - timeOriginEvt.ts) / 1000;
     // This could be much more concise with object spread, but the consensus is that explicitness is
     // preferred over brevity here.
     return {
       frames,
       mainThreadEvents,
+      frameEvents,
       frameTreeEvents,
       processEvents,
       mainFrameIds,
+      timeOriginEvt,
+      timings: {
+        timeOrigin: 0,
+        traceEnd: traceEnd.timing,
+      },
+      timestamps: {
+        timeOrigin: timeOriginEvt.ts,
+        traceEnd: traceEnd.timestamp,
+      },
+    };
+  }
+
+  /**
+   * Finds key navigation trace events and computes timings of events in milliseconds since the time
+   * origin in addition to the standard microsecond monotonic timestamps.
+   * @param {LH.Artifacts.ProcessedTrace} processedTrace
+   * @return {LH.Artifacts.ProcessedNavigation}
+  */
+  static processNavigation(processedTrace) {
+    const {frameEvents, frameTreeEvents, timeOriginEvt, timings, timestamps} = processedTrace;
+
+    // Compute the key frame timings for the main frame.
+    const frameTimings = this.computeNavigationTimingsForFrame(frameEvents, {timeOriginEvt});
+
+    // Compute FCP for all frames.
+    const fcpAllFramesEvt = frameTreeEvents.find(
+      e => e.name === 'firstContentfulPaint' && e.ts > timeOriginEvt.ts
+    );
+
+    if (!fcpAllFramesEvt) {
+      throw this.createNoFirstContentfulPaintError();
+    }
+
+    // Compute LCP for all frames.
+    const lcpAllFramesEvt = this.computeValidLCPAllFrames(frameTreeEvents, timeOriginEvt).lcp;
+
+    /** @param {number} ts */
+    const getTiming = ts => (ts - timeOriginEvt.ts) / 1000;
+    /** @param {number=} ts */
+    const maybeGetTiming = (ts) => ts === undefined ? undefined : getTiming(ts);
+
+    return {
       timings: {
-        timeOrigin: frameTimings.timings.timeOrigin,
+        timeOrigin: timings.timeOrigin,
         firstPaint: frameTimings.timings.firstPaint,
         firstContentfulPaint: frameTimings.timings.firstContentfulPaint,
-        firstContentfulPaintAllFrames: maybeGetTiming(fcpAllFramesEvt && fcpAllFramesEvt.ts),
+        firstContentfulPaintAllFrames: getTiming(fcpAllFramesEvt.ts),
         firstMeaningfulPaint: frameTimings.timings.firstMeaningfulPaint,
         largestContentfulPaint: frameTimings.timings.largestContentfulPaint,
         largestContentfulPaintAllFrames: maybeGetTiming(lcpAllFramesEvt && lcpAllFramesEvt.ts),
-        traceEnd: traceEnd.timing,
         load: frameTimings.timings.load,
         domContentLoaded: frameTimings.timings.domContentLoaded,
+        traceEnd: timings.traceEnd,
       },
       timestamps: {
-        timeOrigin: frameTimings.timestamps.timeOrigin,
+        timeOrigin: timestamps.timeOrigin,
         firstPaint: frameTimings.timestamps.firstPaint,
         firstContentfulPaint: frameTimings.timestamps.firstContentfulPaint,
-        firstContentfulPaintAllFrames: fcpAllFramesEvt && fcpAllFramesEvt.ts,
+        firstContentfulPaintAllFrames: fcpAllFramesEvt.ts,
         firstMeaningfulPaint: frameTimings.timestamps.firstMeaningfulPaint,
         largestContentfulPaint: frameTimings.timestamps.largestContentfulPaint,
         largestContentfulPaintAllFrames: lcpAllFramesEvt && lcpAllFramesEvt.ts,
-        traceEnd: traceEnd.timestamp,
         load: frameTimings.timestamps.load,
         domContentLoaded: frameTimings.timestamps.domContentLoaded,
+        traceEnd: timestamps.traceEnd,
       },
-      timeOriginEvt: frameTimings.timeOriginEvt,
       firstPaintEvt: frameTimings.firstPaintEvt,
       firstContentfulPaintEvt: frameTimings.firstContentfulPaintEvt,
       firstContentfulPaintAllFramesEvt: fcpAllFramesEvt,
@@ -773,7 +807,7 @@ class TraceProcessor {
    * @param {Array<LH.TraceEvent>} frameEvents
    * @param {{timeOriginEvt: LH.TraceEvent}} options
   */
-  static computeKeyTimingsForFrame(frameEvents, options) {
+  static computeNavigationTimingsForFrame(frameEvents, options) {
     const {timeOriginEvt} = options;
 
     // Find our first paint of this frame
@@ -784,6 +818,10 @@ class TraceProcessor {
       e => e.name === 'firstContentfulPaint' && e.ts > timeOriginEvt.ts
     );
 
+    if (!firstContentfulPaint) {
+      throw this.createNoFirstContentfulPaintError();
+    }
+
     // fMP will follow at/after the FP
     let firstMeaningfulPaint = frameEvents.find(
       e => e.name === 'firstMeaningfulPaint' && e.ts > timeOriginEvt.ts
@@ -815,24 +853,26 @@ class TraceProcessor {
 
     /** @param {{ts: number}=} event */
     const getTimestamp = (event) => event && event.ts;
-    /** @type {TraceTimesWithoutFCPAndTraceEnd} */
+    /** @type {TraceNavigationTimesForFrame} */
     const timestamps = {
       timeOrigin: timeOriginEvt.ts,
       firstPaint: getTimestamp(firstPaint),
-      firstContentfulPaint: getTimestamp(firstContentfulPaint),
+      firstContentfulPaint: firstContentfulPaint.ts,
       firstMeaningfulPaint: getTimestamp(firstMeaningfulPaint),
       largestContentfulPaint: getTimestamp(lcpResult.lcp),
       load: getTimestamp(load),
       domContentLoaded: getTimestamp(domContentLoaded),
     };
 
+    /** @param {number} ts */
+    const getTiming = ts => (ts - timeOriginEvt.ts) / 1000;
     /** @param {number=} ts */
-    const maybeGetTiming = (ts) => ts === undefined ? undefined : (ts - timeOriginEvt.ts) / 1000;
-    /** @type {TraceTimesWithoutFCPAndTraceEnd} */
+    const maybeGetTiming = (ts) => ts === undefined ? undefined : getTiming(ts);
+    /** @type {TraceNavigationTimesForFrame} */
     const timings = {
       timeOrigin: 0,
       firstPaint: maybeGetTiming(timestamps.firstPaint),
-      firstContentfulPaint: maybeGetTiming(timestamps.firstContentfulPaint),
+      firstContentfulPaint: getTiming(timestamps.firstContentfulPaint),
       firstMeaningfulPaint: maybeGetTiming(timestamps.firstMeaningfulPaint),
       largestContentfulPaint: maybeGetTiming(timestamps.largestContentfulPaint),
       load: maybeGetTiming(timestamps.load),
diff --git a/lighthouse-core/scripts/lantern/run-once.js b/lighthouse-core/scripts/lantern/run-once.js
index 1cd033787..2effd44e7 100755
--- a/lighthouse-core/scripts/lantern/run-once.js
+++ b/lighthouse-core/scripts/lantern/run-once.js
@@ -18,7 +18,7 @@ async function run() {
   const tracePath = path.resolve(process.cwd(), process.argv[2]);
   const traces = {defaultPass: require(tracePath)};
   const devtoolsLogs = {defaultPass: require(path.resolve(process.cwd(), process.argv[3]))};
-  const artifacts = {traces, devtoolsLogs};
+  const artifacts = {traces, devtoolsLogs, GatherContext: {gatherMode: 'navigation'}};
   const context = {computedCache: new Map(), settings: {locale: 'en-us'}};
 
   // @ts-expect-error - We don't need the full artifacts or context.
diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts
index 230a1fc6c..d343cb699 100644
--- a/types/artifacts.d.ts
+++ b/types/artifacts.d.ts
@@ -141,6 +141,8 @@ declare global {
       FormElements: Artifacts.Form[];
       /** Screenshot of the entire page (rather than just the above the fold content). */
       FullPageScreenshot: Artifacts.FullPageScreenshot | null;
+      /** Information about how Lighthouse artifacts were gathered. */
+      GatherContext: {gatherMode: LH.Gatherer.GatherMode};
       /** Information about event listeners registered on the global object. */
       GlobalListeners: Array<Artifacts.GlobalListener>;
       /** Whether the page ended up on an HTTPS page after attempting to load the HTTP version. */
@@ -611,12 +613,18 @@ declare global {
         devtoolsLog: DevtoolsLog;
         trace: Trace;
         settings: Immutable<Config.Settings>;
+        gatherContext: Artifacts['GatherContext'];
         simulator?: LanternSimulator;
       }
 
       export interface MetricComputationData extends MetricComputationDataInput {
         networkRecords: Array<Artifacts.NetworkRequest>;
-        traceOfTab: TraceOfTab;
+        processedTrace: ProcessedTrace;
+        processedNavigation?: ProcessedNavigation;
+      }
+
+      export interface NavigationMetricComputationData extends MetricComputationData {
+        processedNavigation: ProcessedNavigation;
       }
 
       export interface Metric {
@@ -643,6 +651,11 @@ declare global {
       export type Speedline = speedline.Output<'speedIndex'>;
 
       export interface TraceTimes {
+        timeOrigin: number;
+        traceEnd: number;
+      }
+
+      export interface NavigationTraceTimes {
         timeOrigin: number;
         firstPaint?: number;
         firstContentfulPaint: number;
@@ -655,23 +668,32 @@ declare global {
         domContentLoaded?: number;
       }
 
-      export interface TraceOfTab {
-        /** The raw timestamps of key metric events, in microseconds. */
+      export interface ProcessedTrace {
+        /** The raw timestamps of key events, in microseconds. */
         timestamps: TraceTimes;
-        /** The relative times from navigationStart to key metric events, in milliseconds. */
+        /** The relative times from timeOrigin to key events, in milliseconds. */
         timings: TraceTimes;
         /** The subset of trace events from the page's process, sorted by timestamp. */
         processEvents: Array<TraceEvent>;
         /** The subset of trace events from the page's main thread, sorted by timestamp. */
         mainThreadEvents: Array<TraceEvent>;
+        /** The subset of trace events from the main frame, sorted by timestamp. */
+        frameEvents: Array<TraceEvent>;
         /** The subset of trace events from the main frame and any child frames, sorted by timestamp. */
         frameTreeEvents: Array<TraceEvent>;
         /** IDs for the trace's main frame, process, and thread. */
         mainFrameIds: {pid: number, tid: number, frameId: string};
         /** The list of frames committed in the trace. */
         frames: Array<{id: string, url: string}>;
-        /** The trace event marking the time at which the page load should consider to have begun. Typically the same as the navigationStart but might differ due to SPA navigations, client-side redirects, etc. */
+        /** The trace event marking the time at which the run should consider to have begun. Typically the same as the navigationStart but might differ due to SPA navigations, client-side redirects, etc. In the FR timespan case, this event is injected by Lighthouse itself. */
         timeOriginEvt: TraceEvent;
+      }
+
+      export interface ProcessedNavigation {
+        /** The raw timestamps of key metric events, in microseconds. */
+        timestamps: NavigationTraceTimes;
+        /** The relative times from navigationStart to key metric events, in milliseconds. */
+        timings: NavigationTraceTimes;
         /** The trace event marking firstPaint, if it was found. */
         firstPaintEvt?: TraceEvent;
         /** The trace event marking firstContentfulPaint, if it was found. */
diff --git a/types/audit.d.ts b/types/audit.d.ts
index 687169392..73ccb8b3c 100644
--- a/types/audit.d.ts
+++ b/types/audit.d.ts
@@ -57,6 +57,8 @@ declare global {
       __internalOptionalArtifacts?: Array<keyof Artifacts>;
       /** A string identifying how the score should be interpreted for display. */
       scoreDisplayMode?: Audit.ScoreDisplayMode;
+      /** A list of gather modes that this audit is applicable to. */
+      supportedModes?: Gatherer.GatherMode[],
     }
 
     export interface ByteEfficiencyItem extends Audit.Details.OpportunityItem {


Test patch:
diff --git a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
index 209ccfc3c..c6956674a 100644
--- a/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
+++ b/lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
@@ -1428,6 +1428,9 @@ Object {
         Object {
           "path": "main-document-content",
         },
+        Object {
+          "path": "gather-context",
+        },
         Object {
           "path": "global-listeners",
         },
@@ -1676,7 +1679,11 @@ Object {
       "blankPage": "about:blank",
       "blockedUrlPatterns": Array [],
       "cpuQuietThresholdMs": 1000,
-      "gatherers": Array [],
+      "gatherers": Array [
+        Object {
+          "path": "gather-context",
+        },
+      ],
       "loadFailureMode": "fatal",
       "networkQuietThresholdMs": 1000,
       "passName": "defaultPass",
diff --git a/lighthouse-cli/test/smokehouse/test-definitions/csp/csp-expectations.js b/lighthouse-cli/test/smokehouse/test-definitions/csp/csp-expectations.js
index 53edd12f0..f86232efd 100644
--- a/lighthouse-cli/test/smokehouse/test-definitions/csp/csp-expectations.js
+++ b/lighthouse-cli/test/smokehouse/test-definitions/csp/csp-expectations.js
@@ -63,13 +63,6 @@ module.exports = [
             isReportOnly: false,
             contentSecurityPolicyViolationType: 'kURLViolation',
           },
-          {
-            // TODO: Fix style-src-elem violation when checking tap targets.
-            // https://github.com/GoogleChrome/lighthouse/issues/11862
-            violatedDirective: 'style-src-elem',
-            isReportOnly: false,
-            contentSecurityPolicyViolationType: 'kInlineViolation',
-          },
         ],
       },
       SourceMaps: [{
@@ -98,13 +91,6 @@ module.exports = [
       },
       InspectorIssues: {
         contentSecurityPolicy: [
-          {
-            // TODO: Fix style-src-elem violation when checking tap targets.
-            // https://github.com/GoogleChrome/lighthouse/issues/11862
-            violatedDirective: 'style-src-elem',
-            isReportOnly: false,
-            contentSecurityPolicyViolationType: 'kInlineViolation',
-          },
         ],
       },
       SourceMaps: [{
diff --git a/lighthouse-core/test/audits/byte-efficiency/legacy-javascript-test.js b/lighthouse-core/test/audits/byte-efficiency/legacy-javascript-test.js
index 7110e7b65..784711c1b 100644
--- a/lighthouse-core/test/audits/byte-efficiency/legacy-javascript-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/legacy-javascript-test.js
@@ -22,6 +22,7 @@ const getResult = scripts => {
     })),
   ];
   const artifacts = {
+    GatherContext: {gatherMode: 'navigation'},
     URL: {finalUrl: mainDocumentUrl, requestedUrl: mainDocumentUrl},
     devtoolsLogs: {defaultPass: networkRecordsToDevtoolsLog(networkRecords)},
     ScriptElements: scripts.map(({url, code}, index) => {
diff --git a/lighthouse-core/test/audits/byte-efficiency/modern-image-formats-test.js b/lighthouse-core/test/audits/byte-efficiency/modern-image-formats-test.js
index ab6aa6b57..dfc8ce0b8 100644
--- a/lighthouse-core/test/audits/byte-efficiency/modern-image-formats-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/modern-image-formats-test.js
@@ -37,6 +37,7 @@ function generateArtifacts(images) {
   }
 
   return {
+    GatherContext: {gatherMode: 'navigation'},
     URL: {finalUrl: 'http://google.com/'},
     ImageElements: imageElements,
     OptimizedImages: optimizedImages,
diff --git a/lighthouse-core/test/audits/byte-efficiency/offscreen-images-test.js b/lighthouse-core/test/audits/byte-efficiency/offscreen-images-test.js
index e1e85b9bb..0427c80c0 100644
--- a/lighthouse-core/test/audits/byte-efficiency/offscreen-images-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/offscreen-images-test.js
@@ -72,6 +72,7 @@ describe('OffscreenImages audit', () => {
     const topLevelTasks = [{ts: 1900, duration: 100}];
     const artifacts = {
       ViewportDimensions: DEFAULT_DIMENSIONS,
+      GatherContext: {gatherMode: 'navigation'},
       ImageElements: [
         generateImage({size: generateSize(100, 100), x: 0, y: 0}),
       ],
@@ -93,6 +94,7 @@ describe('OffscreenImages audit', () => {
     const topLevelTasks = [{ts: 1900, duration: 100}];
     const artifacts = {
       ViewportDimensions: DEFAULT_DIMENSIONS,
+      GatherContext: {gatherMode: 'navigation'},
       ImageElements: [
         generateImage({
           size: generateSize(200, 200),
@@ -137,6 +139,7 @@ describe('OffscreenImages audit', () => {
     ];
     const artifacts = {
       ViewportDimensions: DEFAULT_DIMENSIONS,
+      GatherContext: {gatherMode: 'navigation'},
       ImageElements: [
         // Offscreen to the right.
         generateImage({
@@ -218,6 +221,7 @@ describe('OffscreenImages audit', () => {
     ];
     const artifacts = {
       ViewportDimensions: DEFAULT_DIMENSIONS,
+      GatherContext: {gatherMode: 'navigation'},
       ImageElements: [
         // Offscreen to the right, but lazy loaded.
         generateImage({
@@ -256,6 +260,7 @@ describe('OffscreenImages audit', () => {
     ];
     const artifacts = {
       ViewportDimensions: DEFAULT_DIMENSIONS,
+      GatherContext: {gatherMode: 'navigation'},
       ImageElements: [
         // Offscreen to the right with auto loading (same as not specifying the attribute).
         generateImage({
@@ -290,6 +295,7 @@ describe('OffscreenImages audit', () => {
     const networkRecord = generateRecord({resourceSizeInKb: 100});
     const artifacts = {
       ViewportDimensions: DEFAULT_DIMENSIONS,
+      GatherContext: {gatherMode: 'navigation'},
       ImageElements: [
         generateImage({size: generateSize(0, 0), x: 0, y: 0, networkRecord}),
       ],
@@ -314,6 +320,7 @@ describe('OffscreenImages audit', () => {
     ];
     const artifacts = {
       ViewportDimensions: DEFAULT_DIMENSIONS,
+      GatherContext: {gatherMode: 'navigation'},
       ImageElements: [
         generateImage({
           size: generateSize(50, 50),
@@ -356,6 +363,7 @@ describe('OffscreenImages audit', () => {
     const networkRecord = generateRecord({resourceSizeInKb: 100, startTime: 3});
     const artifacts = {
       ViewportDimensions: DEFAULT_DIMENSIONS,
+      GatherContext: {gatherMode: 'navigation'},
       ImageElements: [
         // Offscreen to the right.
         generateImage({size: generateSize(200, 200), x: 3000, y: 0, networkRecord}),
@@ -373,6 +381,7 @@ describe('OffscreenImages audit', () => {
     const networkRecord = generateRecord({resourceSizeInKb: 100, startTime: 3});
     const artifacts = {
       ViewportDimensions: DEFAULT_DIMENSIONS,
+      GatherContext: {gatherMode: 'navigation'},
       ImageElements: [
         // Offscreen to the right.
         generateImage({size: generateSize(200, 200), x: 3000, y: 0, networkRecord}),
@@ -390,6 +399,7 @@ describe('OffscreenImages audit', () => {
     const networkRecord = generateRecord({resourceSizeInKb: 100});
     const artifacts = {
       ViewportDimensions: DEFAULT_DIMENSIONS,
+      GatherContext: {gatherMode: 'navigation'},
       ImageElements: [
         // Offscreen to the bottom.
         generateImage({size: generateSize(100, 100), x: 0, y: 5000, networkRecord}),
@@ -431,6 +441,7 @@ describe('OffscreenImages audit', () => {
     ];
     const artifacts = {
       ViewportDimensions: DEFAULT_DIMENSIONS,
+      GatherContext: {gatherMode: 'navigation'},
       ImageElements: [
         generateImage({
           size: generateSize(0, 0),
@@ -489,6 +500,7 @@ describe('OffscreenImages audit', () => {
     ];
     const artifacts = {
       ViewportDimensions: DEFAULT_DIMENSIONS,
+      GatherContext: {gatherMode: 'navigation'},
       ImageElements: [
         generateImage({
           size: generateSize(0, 0),
@@ -526,6 +538,7 @@ describe('OffscreenImages audit', () => {
     ];
     const artifacts = {
       ViewportDimensions: DEFAULT_DIMENSIONS,
+      GatherContext: {gatherMode: 'navigation'},
       ImageElements: [
         generateImage({
           size: generateSize(0, 0),
diff --git a/lighthouse-core/test/audits/byte-efficiency/render-blocking-resources-test.js b/lighthouse-core/test/audits/byte-efficiency/render-blocking-resources-test.js
index 157197db7..b65731569 100644
--- a/lighthouse-core/test/audits/byte-efficiency/render-blocking-resources-test.js
+++ b/lighthouse-core/test/audits/byte-efficiency/render-blocking-resources-test.js
@@ -25,6 +25,7 @@ const ampDevtoolsLog = require('../../fixtures/traces/amp-m86.devtoolslog.json')
 describe('Render blocking resources audit', () => {
   it('evaluates http2 input correctly', async () => {
     const artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
       traces: {defaultPass: trace},
       devtoolsLogs: {defaultPass: devtoolsLog},
       TagsBlockingFirstPaint: [
@@ -44,6 +45,7 @@ describe('Render blocking resources audit', () => {
 
   it('evaluates amp page correctly', async () => {
     const artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
       traces: {defaultPass: ampTrace},
       devtoolsLogs: {defaultPass: ampDevtoolsLog},
       TagsBlockingFirstPaint: [
diff --git a/lighthouse-core/test/audits/metrics-test.js b/lighthouse-core/test/audits/metrics-test.js
index 93f8f3f7e..92e311d2f 100644
--- a/lighthouse-core/test/audits/metrics-test.js
+++ b/lighthouse-core/test/audits/metrics-test.js
@@ -28,6 +28,7 @@ const jumpyClsDevtoolsLog = require('../fixtures/traces/jumpy-cls-m90.devtoolslo
 describe('Performance: metrics', () => {
   it('evaluates valid input correctly', async () => {
     const artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
       traces: {
         [MetricsAudit.DEFAULT_PASS]: pwaTrace,
       },
@@ -43,6 +44,7 @@ describe('Performance: metrics', () => {
 
   it('evaluates valid input correctly (throttlingMethod=provided)', async () => {
     const artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
       traces: {
         [MetricsAudit.DEFAULT_PASS]: pwaTrace,
       },
@@ -58,6 +60,7 @@ describe('Performance: metrics', () => {
 
   it('evaluates valid input (with lcp) correctly', async () => {
     const artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
       traces: {
         [MetricsAudit.DEFAULT_PASS]: lcpTrace,
       },
@@ -73,6 +76,7 @@ describe('Performance: metrics', () => {
 
   it('evaluates valid input (with lcp from all frames) correctly', async () => {
     const artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
       traces: {
         [MetricsAudit.DEFAULT_PASS]: lcpAllFramesTrace,
       },
@@ -88,6 +92,7 @@ describe('Performance: metrics', () => {
 
   it('leaves CLS undefined in an old trace without weighted scores', async () => {
     const artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
       traces: {
         [MetricsAudit.DEFAULT_PASS]: lcpAllFramesTrace,
       },
@@ -110,6 +115,7 @@ describe('Performance: metrics', () => {
 
   it('evaluates new CLS correctly across all frames', async () => {
     const artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
       traces: {
         [MetricsAudit.DEFAULT_PASS]: clsAllFramesTrace,
       },
@@ -135,6 +141,7 @@ describe('Performance: metrics', () => {
 
   it('does not fail the entire audit when TTI errors', async () => {
     const artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
       traces: {
         [MetricsAudit.DEFAULT_PASS]: pwaTrace,
       },
@@ -152,6 +159,7 @@ describe('Performance: metrics', () => {
 
   it('evaluates CLS correctly', async () => {
     const artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
       traces: {
         [MetricsAudit.DEFAULT_PASS]: jumpyClsTrace,
       },
diff --git a/lighthouse-core/test/audits/metrics/cumulative-layout-shift-test.js b/lighthouse-core/test/audits/metrics/cumulative-layout-shift-test.js
index 67875589a..647635f3d 100644
--- a/lighthouse-core/test/audits/metrics/cumulative-layout-shift-test.js
+++ b/lighthouse-core/test/audits/metrics/cumulative-layout-shift-test.js
@@ -13,6 +13,7 @@ const jumpyClsTrace = require('../../fixtures/traces/jumpy-cls-m90.json');
 describe('Cumulative Layout Shift', () => {
   it('evaluates CLS correctly', async () => {
     const artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
       traces: {
         [CumulativeLayoutShift.DEFAULT_PASS]: jumpyClsTrace,
       },
diff --git a/lighthouse-core/test/audits/metrics/first-contentful-paint-3g-test.js b/lighthouse-core/test/audits/metrics/first-contentful-paint-3g-test.js
index beae2bb98..3c3858202 100644
--- a/lighthouse-core/test/audits/metrics/first-contentful-paint-3g-test.js
+++ b/lighthouse-core/test/audits/metrics/first-contentful-paint-3g-test.js
@@ -16,6 +16,7 @@ const pwaDevtoolsLog = require('../../fixtures/traces/progressive-app-m60.devtoo
 describe('Performance: first-contentful-paint-3g audit', () => {
   it('evaluates valid input correctly', async () => {
     const artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
       traces: {
         [FCP3G.DEFAULT_PASS]: pwaTrace,
       },
diff --git a/lighthouse-core/test/audits/metrics/first-contentful-paint-test.js b/lighthouse-core/test/audits/metrics/first-contentful-paint-test.js
index 9e7604029..d05887663 100644
--- a/lighthouse-core/test/audits/metrics/first-contentful-paint-test.js
+++ b/lighthouse-core/test/audits/metrics/first-contentful-paint-test.js
@@ -37,6 +37,7 @@ const getFakeContext = ({formFactor, throttlingMethod}) => ({
 describe('Performance: first-contentful-paint audit', () => {
   it('evaluates valid input correctly', async () => {
     const artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
       traces: {
         [FcpAudit.DEFAULT_PASS]: pwaTrace,
       },
@@ -53,6 +54,7 @@ describe('Performance: first-contentful-paint audit', () => {
 
   it('evaluates a modern trace correctly', async () => {
     const artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
       traces: {
         [FcpAudit.DEFAULT_PASS]: frameTrace,
       },
diff --git a/lighthouse-core/test/audits/metrics/first-meaningful-paint-test.js b/lighthouse-core/test/audits/metrics/first-meaningful-paint-test.js
index fa59ad274..f0ad85cb3 100644
--- a/lighthouse-core/test/audits/metrics/first-meaningful-paint-test.js
+++ b/lighthouse-core/test/audits/metrics/first-meaningful-paint-test.js
@@ -33,6 +33,7 @@ const getFakeContext = ({formFactor, throttlingMethod}) => ({
 describe('Performance: first-meaningful-paint audit', () => {
   it('computes FMP correctly for valid trace', async () => {
     const artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
       traces: {[Audit.DEFAULT_PASS]: trace},
       devtoolsLogs: {[Audit.DEFAULT_PASS]: devtoolsLogs},
     };
@@ -46,6 +47,7 @@ describe('Performance: first-meaningful-paint audit', () => {
 
   it('computes FMP correctly for simulated', async () => {
     const artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
       traces: {[Audit.DEFAULT_PASS]: trace},
       devtoolsLogs: {[Audit.DEFAULT_PASS]: devtoolsLogs},
     };
diff --git a/lighthouse-core/test/audits/metrics/interactive-test.js b/lighthouse-core/test/audits/metrics/interactive-test.js
index 8abeeca6d..446002ba9 100644
--- a/lighthouse-core/test/audits/metrics/interactive-test.js
+++ b/lighthouse-core/test/audits/metrics/interactive-test.js
@@ -37,6 +37,7 @@ const getFakeContext = ({formFactor, throttlingMethod}) => ({
 describe('Performance: interactive audit', () => {
   it('should compute interactive', () => {
     const artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
       traces: {
         [Interactive.DEFAULT_PASS]: acceptableTrace,
       },
@@ -55,6 +56,7 @@ describe('Performance: interactive audit', () => {
 
   it('should compute interactive on pages with redirect', () => {
     const artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
       traces: {
         [Interactive.DEFAULT_PASS]: redirectTrace,
       },
diff --git a/lighthouse-core/test/audits/metrics/largest-contentful-paint-test.js b/lighthouse-core/test/audits/metrics/largest-contentful-paint-test.js
index a35da8e0b..22a58f127 100644
--- a/lighthouse-core/test/audits/metrics/largest-contentful-paint-test.js
+++ b/lighthouse-core/test/audits/metrics/largest-contentful-paint-test.js
@@ -17,6 +17,7 @@ const preLcpDevtoolsLog = require('../../fixtures/traces/progressive-app-m60.dev
 
 function generateArtifacts({trace, devtoolsLog, HostUserAgent}) {
   return {
+    GatherContext: {gatherMode: 'navigation'},
     traces: {[LCPAudit.DEFAULT_PASS]: trace},
     devtoolsLogs: {[LCPAudit.DEFAULT_PASS]: devtoolsLog},
     HostUserAgent,
diff --git a/lighthouse-core/test/audits/metrics/speed-index-test.js b/lighthouse-core/test/audits/metrics/speed-index-test.js
index 8d77fb3d1..e43659ddf 100644
--- a/lighthouse-core/test/audits/metrics/speed-index-test.js
+++ b/lighthouse-core/test/audits/metrics/speed-index-test.js
@@ -35,6 +35,7 @@ const getFakeContext = ({formFactor, throttlingMethod}) => ({
 describe('Performance: speed-index audit', () => {
   it('works on a real trace', () => {
     const artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
       traces: {defaultPass: pwaTrace},
       devtoolsLogs: {defaultPass: pwaDevtoolsLog},
     };
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 d051edabd..f5ff6b35b 100644
--- a/lighthouse-core/test/audits/metrics/total-blocking-time-test.js
+++ b/lighthouse-core/test/audits/metrics/total-blocking-time-test.js
@@ -17,6 +17,7 @@ const lcpDevtoolsLog = require('../../fixtures/traces/lcp-m78.devtools.log.json'
 
 function generateArtifacts({trace, devtoolsLog}) {
   return {
+    GatherContext: {gatherMode: 'navigation'},
     traces: {[TBTAudit.DEFAULT_PASS]: trace},
     devtoolsLogs: {[TBTAudit.DEFAULT_PASS]: devtoolsLog},
   };
diff --git a/lighthouse-core/test/audits/predictive-perf-test.js b/lighthouse-core/test/audits/predictive-perf-test.js
index f1b104c3c..9d0209e6e 100644
--- a/lighthouse-core/test/audits/predictive-perf-test.js
+++ b/lighthouse-core/test/audits/predictive-perf-test.js
@@ -14,6 +14,7 @@ const acceptableDevToolsLog = require('../fixtures/traces/lcp-m78.devtools.log.j
 describe('Performance: predictive performance audit', () => {
   it('should compute the predicted values', async () => {
     const artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
       traces: {
         [PredictivePerf.DEFAULT_PASS]: acceptableTrace,
       },
diff --git a/lighthouse-core/test/audits/preload-lcp-image-test.js b/lighthouse-core/test/audits/preload-lcp-image-test.js
index bec3bb0f0..2603e4d8e 100644
--- a/lighthouse-core/test/audits/preload-lcp-image-test.js
+++ b/lighthouse-core/test/audits/preload-lcp-image-test.js
@@ -20,6 +20,7 @@ const imageUrl = 'http://www.example.com/image.png';
 describe('Performance: preload-lcp audit', () => {
   const mockArtifacts = (networkRecords, finalUrl, imageUrl) => {
     return {
+      GatherContext: {gatherMode: 'navigation'},
       traces: {
         [PreloadLCPImage.DEFAULT_PASS]: createTestTrace({
           traceEnd: 6e3,
diff --git a/lighthouse-core/test/audits/redirects-test.js b/lighthouse-core/test/audits/redirects-test.js
index 71f345fc6..de263bb92 100644
--- a/lighthouse-core/test/audits/redirects-test.js
+++ b/lighthouse-core/test/audits/redirects-test.js
@@ -109,6 +109,7 @@ describe('Performance: Redirects audit', () => {
     const devtoolsLog = networkRecordsToDevtoolsLog(networkRecords);
 
     return {
+      GatherContext: {gatherMode: 'navigation'},
       traces: {defaultPass: createTestTrace({traceEnd: 5000})},
       devtoolsLogs: {defaultPass: devtoolsLog},
       URL: {finalUrl},
diff --git a/lighthouse-core/test/audits/screenshot-thumbnails-test.js b/lighthouse-core/test/audits/screenshot-thumbnails-test.js
index ea3a3368d..26b84e509 100644
--- a/lighthouse-core/test/audits/screenshot-thumbnails-test.js
+++ b/lighthouse-core/test/audits/screenshot-thumbnails-test.js
@@ -42,7 +42,7 @@ describe('Screenshot thumbnails', () => {
     });
   }, 10000);
 
-  it('should scale the timeline to TTI when observed', () => {
+  it('should scale the timeline to last visual change', () => {
     const options = {minimumTimelineDuration: 500};
     const settings = {throttlingMethod: 'devtools'};
     const artifacts = {
@@ -50,25 +50,6 @@ describe('Screenshot thumbnails', () => {
       devtoolsLogs: {defaultPass: pwaDevtoolsLog},
     };
 
-    const context = {settings, options, computedCache: new Map()};
-    return ScreenshotThumbnailsAudit.audit(artifacts, context).then(results => {
-      assert.equal(results.details.items[0].timing, 158);
-      assert.equal(results.details.items[9].timing, 1582);
-
-      // last 5 frames should be equal to the last real frame
-      const extrapolatedFrames = new Set(results.details.items.slice(5).map(f => f.data));
-      assert.ok(results.details.items[9].data.length > 100, 'did not have last frame');
-      assert.ok(extrapolatedFrames.size === 1, 'did not extrapolate last frame');
-    });
-  });
-
-  it('should not scale the timeline to TTI when simulate', () => {
-    const options = {minimumTimelineDuration: 500};
-    const settings = {throttlingMethod: 'simulate'};
-    const artifacts = {
-      traces: {defaultPass: pwaTrace},
-    };
-
     const context = {settings, options, computedCache: new Map()};
     return ScreenshotThumbnailsAudit.audit(artifacts, context).then(results => {
       assert.equal(results.details.items[0].timing, 82);
diff --git a/lighthouse-core/test/audits/timing-budget-test.js b/lighthouse-core/test/audits/timing-budget-test.js
index ef5da8461..62f91da31 100644
--- a/lighthouse-core/test/audits/timing-budget-test.js
+++ b/lighthouse-core/test/audits/timing-budget-test.js
@@ -18,6 +18,7 @@ describe('Performance: Timing budget audit', () => {
   let context;
   beforeEach(() => {
     artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
       devtoolsLogs: {
         defaultPass: devtoolsLog,
       },
diff --git a/lighthouse-core/test/computed/metrics/first-contentful-paint-all-frames-test.js b/lighthouse-core/test/computed/metrics/first-contentful-paint-all-frames-test.js
index 8293fc238..52f229198 100644
--- a/lighthouse-core/test/computed/metrics/first-contentful-paint-all-frames-test.js
+++ b/lighthouse-core/test/computed/metrics/first-contentful-paint-all-frames-test.js
@@ -13,11 +13,13 @@ const devtoolsLog = require('../../fixtures/traces/frame-metrics-m89.devtools.lo
 /* eslint-env jest */
 
 describe('Metrics: FCP all frames', () => {
+  const gatherContext = {gatherMode: 'navigation'};
+
   it('should throw for simulated throttling', async () => {
     const settings = {throttlingMethod: 'simulate'};
     const context = {settings, computedCache: new Map()};
     const resultPromise = FirstContentfulPaintAllFrames.request(
-      {trace, devtoolsLog, settings},
+      {trace, devtoolsLog, gatherContext, settings},
       context
     );
 
@@ -30,11 +32,11 @@ describe('Metrics: FCP all frames', () => {
     const context = {settings, computedCache: new Map()};
 
     const result = await FirstContentfulPaintAllFrames.request(
-      {trace, devtoolsLog, settings},
+      {trace, devtoolsLog, gatherContext, settings},
       context
     );
     const mainFrameResult = await FirstContentfulPaint.request(
-      {trace, devtoolsLog, settings},
+      {trace, devtoolsLog, gatherContext, settings},
       context
     );
 
diff --git a/lighthouse-core/test/computed/metrics/first-contentful-paint-test.js b/lighthouse-core/test/computed/metrics/first-contentful-paint-test.js
index 4eb38adef..32f82478a 100644
--- a/lighthouse-core/test/computed/metrics/first-contentful-paint-test.js
+++ b/lighthouse-core/test/computed/metrics/first-contentful-paint-test.js
@@ -14,10 +14,13 @@ const devtoolsLog = require('../../fixtures/traces/progressive-app-m60.devtools.
 /* eslint-env jest */
 
 describe('Metrics: FCP', () => {
+  const gatherContext = {gatherMode: 'navigation'};
+
   it('should compute a simulated value', async () => {
     const settings = {throttlingMethod: 'simulate'};
     const context = {settings, computedCache: new Map()};
-    const result = await FirstContentfulPaint.request({trace, devtoolsLog, settings}, context);
+    const result = await FirstContentfulPaint.request({trace, devtoolsLog, gatherContext, settings},
+      context);
 
     expect({
       timing: Math.round(result.timing),
@@ -33,7 +36,8 @@ describe('Metrics: FCP', () => {
   it('should compute an observed value (desktop)', async () => {
     const settings = {throttlingMethod: 'provided', formFactor: 'desktop'};
     const context = {settings, computedCache: new Map()};
-    const result = await FirstContentfulPaint.request({trace, devtoolsLog, settings}, context);
+    const result = await FirstContentfulPaint.request({trace, devtoolsLog, gatherContext, settings},
+      context);
 
     assert.equal(Math.round(result.timing), 499);
     assert.equal(result.timestamp, 225414670885);
@@ -43,7 +47,7 @@ describe('Metrics: FCP', () => {
     const settings = {throttlingMethod: 'provided', formFactor: 'mobile'};
     const context = {settings, computedCache: new Map()};
     const result = await FirstContentfulPaint.request(
-      {trace, devtoolsLog, settings}, context);
+      {gatherContext, trace, devtoolsLog, settings}, context);
 
     assert.equal(Math.round(result.timing), 499);
     assert.equal(result.timestamp, 225414670885);
diff --git a/lighthouse-core/test/computed/metrics/first-meaningful-paint-test.js b/lighthouse-core/test/computed/metrics/first-meaningful-paint-test.js
index 998b99d6c..7ec631d38 100644
--- a/lighthouse-core/test/computed/metrics/first-meaningful-paint-test.js
+++ b/lighthouse-core/test/computed/metrics/first-meaningful-paint-test.js
@@ -20,6 +20,7 @@ const noFMPtrace = require(`${TRACE_FIXTURES}/no_fmp_event.json`);
 /* eslint-env jest */
 
 describe('Metrics: FMP', () => {
+  const gatherContext = {gatherMode: 'navigation'};
   let settings;
   let trace;
   let devtoolsLog;
@@ -44,7 +45,8 @@ describe('Metrics: FMP', () => {
     devtoolsLog = pwaDevtoolsLog;
 
     const context = {computedCache: new Map()};
-    const result = await FirstMeaningfulPaint.request({trace, devtoolsLog, settings}, context);
+    const result = await FirstMeaningfulPaint.request({trace, devtoolsLog, gatherContext, settings},
+      context);
 
     expect({
       timing: Math.round(result.timing),
@@ -60,7 +62,8 @@ describe('Metrics: FMP', () => {
   it('should compute an observed value (desktop)', async () => {
     settings = {throttlingMethod: 'provided', formFactor: 'desktop'};
     const context = {computedCache: new Map()};
-    const result = await FirstMeaningfulPaint.request({trace, devtoolsLog, settings}, context);
+    const result = await FirstMeaningfulPaint.request({trace, devtoolsLog, gatherContext, settings},
+      context);
 
     assert.equal(Math.round(result.timing), 783);
     assert.equal(result.timestamp, 225414955343);
@@ -69,7 +72,8 @@ describe('Metrics: FMP', () => {
   it('should compute an observed value (mobile)', async () => {
     settings = {throttlingMethod: 'provided', formFactor: 'mobile'};
     const context = {computedCache: new Map()};
-    const result = await FirstMeaningfulPaint.request({trace, devtoolsLog, settings}, context);
+    const result = await FirstMeaningfulPaint.request({trace, devtoolsLog, gatherContext, settings},
+      context);
 
     assert.equal(Math.round(result.timing), 783);
     assert.equal(result.timestamp, 225414955343);
@@ -79,7 +83,8 @@ describe('Metrics: FMP', () => {
     trace = lateTracingStartedTrace;
     addEmptyTask();
     const context = {computedCache: new Map()};
-    const result = await FirstMeaningfulPaint.request({trace, devtoolsLog, settings}, context);
+    const result = await FirstMeaningfulPaint.request({trace, devtoolsLog, gatherContext, settings},
+      context);
     assert.equal(Math.round(result.timing), 530);
     assert.equal(result.timestamp, 29344070867);
   });
@@ -88,7 +93,8 @@ describe('Metrics: FMP', () => {
     trace = badNavStartTrace;
     addEmptyTask();
     const context = {computedCache: new Map()};
-    const result = await FirstMeaningfulPaint.request({trace, devtoolsLog, settings}, context);
+    const result = await FirstMeaningfulPaint.request({trace, devtoolsLog, gatherContext, settings},
+      context);
     assert.equal(Math.round(result.timing), 632);
     assert.equal(result.timestamp, 8886056891);
   });
@@ -97,7 +103,8 @@ describe('Metrics: FMP', () => {
     trace = preactTrace;
     addEmptyTask();
     const context = {computedCache: new Map()};
-    const result = await FirstMeaningfulPaint.request({trace, devtoolsLog, settings}, context);
+    const result = await FirstMeaningfulPaint.request({trace, devtoolsLog, gatherContext, settings},
+      context);
     assert.equal(Math.round(result.timing), 878);
     assert.equal(result.timestamp, 1805797262960);
   });
@@ -106,7 +113,8 @@ describe('Metrics: FMP', () => {
     trace = noFMPtrace;
     addEmptyTask();
     const context = {computedCache: new Map()};
-    const result = await FirstMeaningfulPaint.request({trace, devtoolsLog, settings}, context);
+    const result = await FirstMeaningfulPaint.request({trace, devtoolsLog, gatherContext, settings},
+      context);
     assert.equal(Math.round(result.timing), 4461);
     assert.equal(result.timestamp, 2146740268666);
   });
diff --git a/lighthouse-core/test/computed/metrics/interactive-test.js b/lighthouse-core/test/computed/metrics/interactive-test.js
index bed0b5c3e..c8ac62c73 100644
--- a/lighthouse-core/test/computed/metrics/interactive-test.js
+++ b/lighthouse-core/test/computed/metrics/interactive-test.js
@@ -29,10 +29,13 @@ function generateNetworkRecords(records, timeOrigin) {
 
 /* eslint-env jest */
 describe('Metrics: TTI', () => {
+  const gatherContext = {gatherMode: 'navigation'};
+
   it('should compute a simulated value', async () => {
     const settings = {throttlingMethod: 'simulate'};
     const context = {settings, computedCache: new Map()};
-    const result = await Interactive.request({trace, devtoolsLog, settings}, context);
+    const result = await Interactive.request({trace, devtoolsLog, gatherContext, settings},
+      context);
 
     expect({
       timing: Math.round(result.timing),
@@ -48,7 +51,8 @@ describe('Metrics: TTI', () => {
   it('should compute an observed value (desktop)', async () => {
     const settings = {throttlingMethod: 'provided', formFactor: 'desktop'};
     const context = {settings, computedCache: new Map()};
-    const result = await Interactive.request({trace, devtoolsLog, settings}, context);
+    const result = await Interactive.request({trace, devtoolsLog, gatherContext, settings},
+      context);
 
     assert.equal(Math.round(result.timing), 1582);
     assert.equal(result.timestamp, 225415754204);
@@ -57,7 +61,8 @@ describe('Metrics: TTI', () => {
   it('should compute an observed value (mobile)', async () => {
     const settings = {throttlingMethod: 'provided', formFactor: 'mobile'};
     const context = {settings, computedCache: new Map()};
-    const result = await Interactive.request({trace, devtoolsLog, settings}, context);
+    const result = await Interactive.request({trace, devtoolsLog, gatherContext, settings},
+      context);
 
     assert.equal(Math.round(result.timing), 1582);
     assert.equal(result.timestamp, 225415754204);
@@ -68,12 +73,12 @@ describe('Metrics: TTI', () => {
       const timeOrigin = 220023532;
       const firstContentfulPaint = 2500 * 1000 + timeOrigin;
       const traceEnd = 10000 * 1000 + timeOrigin;
-      const traceOfTab = {timestamps: {timeOrigin, firstContentfulPaint, traceEnd}};
+      const processedTrace = {timestamps: {timeOrigin, firstContentfulPaint, traceEnd}};
 
       const cpu = [];
       const network = generateNetworkRecords([], timeOrigin);
 
-      const result = Interactive.findOverlappingQuietPeriods(cpu, network, traceOfTab);
+      const result = Interactive.findOverlappingQuietPeriods(cpu, network, processedTrace);
       assert.deepEqual(result.cpuQuietPeriod, {start: 0, end: traceEnd / 1000});
       assert.deepEqual(result.networkQuietPeriod, {start: 0, end: traceEnd / 1000});
     });
@@ -82,13 +87,13 @@ describe('Metrics: TTI', () => {
       const timeOrigin = 220023532;
       const firstContentfulPaint = 2500 * 1000 + timeOrigin;
       const traceEnd = 5000 * 1000 + timeOrigin;
-      const traceOfTab = {timestamps: {timeOrigin, firstContentfulPaint, traceEnd}};
+      const processedTrace = {timestamps: {timeOrigin, firstContentfulPaint, traceEnd}};
 
       const cpu = [];
       const network = generateNetworkRecords([], timeOrigin);
 
       assert.throws(() => {
-        Interactive.findOverlappingQuietPeriods(cpu, network, traceOfTab);
+        Interactive.findOverlappingQuietPeriods(cpu, network, processedTrace);
       }, /NO.*IDLE_PERIOD/);
     });
 
@@ -96,7 +101,7 @@ describe('Metrics: TTI', () => {
       const timeOrigin = 220023532;
       const firstContentfulPaint = 2500 * 1000 + timeOrigin;
       const traceEnd = 10000 * 1000 + timeOrigin;
-      const traceOfTab = {timestamps: {timeOrigin, firstContentfulPaint, traceEnd}};
+      const processedTrace = {timestamps: {timeOrigin, firstContentfulPaint, traceEnd}};
 
       const cpu = [];
       const network = generateNetworkRecords([
@@ -107,7 +112,7 @@ describe('Metrics: TTI', () => {
       ], timeOrigin);
 
       assert.throws(() => {
-        Interactive.findOverlappingQuietPeriods(cpu, network, traceOfTab);
+        Interactive.findOverlappingQuietPeriods(cpu, network, processedTrace);
       }, /NO.*NETWORK_IDLE_PERIOD/);
     });
 
@@ -115,7 +120,7 @@ describe('Metrics: TTI', () => {
       const timeOrigin = 220023532;
       const firstContentfulPaint = 2500 * 1000 + timeOrigin;
       const traceEnd = 10000 * 1000 + timeOrigin;
-      const traceOfTab = {timestamps: {timeOrigin, firstContentfulPaint, traceEnd}};
+      const processedTrace = {timestamps: {timeOrigin, firstContentfulPaint, traceEnd}};
 
       const cpu = [
         {start: 3000, end: 8000},
@@ -125,7 +130,7 @@ describe('Metrics: TTI', () => {
       ], timeOrigin);
 
       assert.throws(() => {
-        Interactive.findOverlappingQuietPeriods(cpu, network, traceOfTab);
+        Interactive.findOverlappingQuietPeriods(cpu, network, processedTrace);
       }, /NO.*CPU_IDLE_PERIOD/);
     });
 
@@ -133,7 +138,7 @@ describe('Metrics: TTI', () => {
       const timeOrigin = 220023532;
       const firstContentfulPaint = 2500 * 1000 + timeOrigin;
       const traceEnd = 10000 * 1000 + timeOrigin;
-      const traceOfTab = {timestamps: {timeOrigin, firstContentfulPaint, traceEnd}};
+      const processedTrace = {timestamps: {timeOrigin, firstContentfulPaint, traceEnd}};
 
       const cpu = [];
       let network = generateNetworkRecords([
@@ -145,7 +150,7 @@ describe('Metrics: TTI', () => {
       // Triple the requests to ensure it's not just the 2-quiet kicking in
       network = network.concat(network).concat(network);
 
-      const result = Interactive.findOverlappingQuietPeriods(cpu, network, traceOfTab);
+      const result = Interactive.findOverlappingQuietPeriods(cpu, network, processedTrace);
       assert.deepEqual(result.cpuQuietPeriod, {start: 0, end: traceEnd / 1000});
       assert.deepEqual(result.networkQuietPeriod, {start: 0, end: traceEnd / 1000});
     });
@@ -154,7 +159,7 @@ describe('Metrics: TTI', () => {
       const timeOrigin = 220023532;
       const firstContentfulPaint = 10000 * 1000 + timeOrigin;
       const traceEnd = 45000 * 1000 + timeOrigin;
-      const traceOfTab = {timestamps: {timeOrigin, firstContentfulPaint, traceEnd}};
+      const processedTrace = {timestamps: {timeOrigin, firstContentfulPaint, traceEnd}};
 
       const cpu = [
         // quiet period before FMP
@@ -184,7 +189,7 @@ describe('Metrics: TTI', () => {
         // final quiet period
       ], timeOrigin);
 
-      const result = Interactive.findOverlappingQuietPeriods(cpu, network, traceOfTab);
+      const result = Interactive.findOverlappingQuietPeriods(cpu, network, processedTrace);
       assert.deepEqual(result.cpuQuietPeriod, {
         start: 34000 + timeOrigin / 1000,
         end: traceEnd / 1000,
diff --git a/lighthouse-core/test/computed/metrics/lantern-first-contentful-paint-test.js b/lighthouse-core/test/computed/metrics/lantern-first-contentful-paint-test.js
index 8b0819ab1..8eac3c76c 100644
--- a/lighthouse-core/test/computed/metrics/lantern-first-contentful-paint-test.js
+++ b/lighthouse-core/test/computed/metrics/lantern-first-contentful-paint-test.js
@@ -13,10 +13,11 @@ const devtoolsLog = require('../../fixtures/traces/progressive-app-m60.devtools.
 
 /* eslint-env jest */
 describe('Metrics: Lantern FCP', () => {
+  const gatherContext = {gatherMode: 'navigation'};
   it('should compute predicted value', async () => {
     const settings = {};
     const context = {settings, computedCache: new Map()};
-    const result = await LanternFirstContentfulPaint.request({trace, devtoolsLog,
+    const result = await LanternFirstContentfulPaint.request({trace, devtoolsLog, gatherContext,
       settings}, context);
 
     expect({
diff --git a/lighthouse-core/test/computed/metrics/lantern-first-meaningful-paint-test.js b/lighthouse-core/test/computed/metrics/lantern-first-meaningful-paint-test.js
index 1ab8e2e18..a12b4bd06 100644
--- a/lighthouse-core/test/computed/metrics/lantern-first-meaningful-paint-test.js
+++ b/lighthouse-core/test/computed/metrics/lantern-first-meaningful-paint-test.js
@@ -15,8 +15,9 @@ const LanternFirstMeaningfulPaint =
 /* eslint-env jest */
 describe('Metrics: Lantern FMP', () => {
   it('should compute predicted value', async () => {
+    const gatherContext = {gatherMode: 'navigation'};
     const computedCache = new Map();
-    const result = await LanternFirstMeaningfulPaint.request({trace, devtoolsLog,
+    const result = await LanternFirstMeaningfulPaint.request({trace, devtoolsLog, gatherContext,
       settings: {}}, {computedCache});
 
     expect({
diff --git a/lighthouse-core/test/computed/metrics/lantern-interactive-test.js b/lighthouse-core/test/computed/metrics/lantern-interactive-test.js
index 470dbe7a0..3734c05fd 100644
--- a/lighthouse-core/test/computed/metrics/lantern-interactive-test.js
+++ b/lighthouse-core/test/computed/metrics/lantern-interactive-test.js
@@ -16,10 +16,12 @@ const iframeDevtoolsLog = require('../../fixtures/traces/iframe-m79.devtoolslog.
 /* eslint-env jest */
 
 describe('Metrics: Lantern TTI', () => {
+  const gatherContext = {gatherMode: 'navigation'};
+
   it('should compute predicted value', async () => {
     const settings = {};
     const context = {settings, computedCache: new Map()};
-    const result = await LanternInteractive.request({trace, devtoolsLog,
+    const result = await LanternInteractive.request({trace, devtoolsLog, gatherContext,
       settings}, context);
 
     expect({
@@ -39,6 +41,7 @@ describe('Metrics: Lantern TTI', () => {
     const result = await LanternInteractive.request({
       trace: iframeTrace,
       devtoolsLog: iframeDevtoolsLog,
+      gatherContext,
       settings,
     }, context);
 
diff --git a/lighthouse-core/test/computed/metrics/lantern-largest-contentful-paint-test.js b/lighthouse-core/test/computed/metrics/lantern-largest-contentful-paint-test.js
index e362fdfd5..e79b4cf99 100644
--- a/lighthouse-core/test/computed/metrics/lantern-largest-contentful-paint-test.js
+++ b/lighthouse-core/test/computed/metrics/lantern-largest-contentful-paint-test.js
@@ -14,10 +14,11 @@ const LanternLargestContentfulPaint = require('../../../computed/metrics/lantern
 /* eslint-env jest */
 describe('Metrics: Lantern LCP', () => {
   it('should compute predicted value', async () => {
+    const gatherContext = {gatherMode: 'navigation'};
     const settings = {};
     const computedCache = new Map();
     const result = await LanternLargestContentfulPaint.request(
-      {trace, devtoolsLog, settings},
+      {trace, devtoolsLog, gatherContext, settings},
       {computedCache}
     );
 
diff --git a/lighthouse-core/test/computed/metrics/lantern-speed-index-test.js b/lighthouse-core/test/computed/metrics/lantern-speed-index-test.js
index c7cbaa160..e7443c93c 100644
--- a/lighthouse-core/test/computed/metrics/lantern-speed-index-test.js
+++ b/lighthouse-core/test/computed/metrics/lantern-speed-index-test.js
@@ -13,10 +13,12 @@ const devtoolsLog = require('../../fixtures/traces/progressive-app-m60.devtools.
 
 /* eslint-env jest */
 describe('Metrics: Lantern Speed Index', () => {
+  const gatherContext = {gatherMode: 'navigation'};
   it('should compute predicted value', async () => {
     const settings = {throttlingMethod: 'simulate', throttling: defaultThrottling};
     const context = {settings, computedCache: new Map()};
-    const result = await LanternSpeedIndex.request({trace, devtoolsLog, settings}, context);
+    const result = await LanternSpeedIndex.request({trace, devtoolsLog, gatherContext, settings},
+      context);
 
     expect({
       timing: Math.round(result.timing),
@@ -34,7 +36,8 @@ describe('Metrics: Lantern Speed Index', () => {
   it('should compute predicted value for different settings', async () => {
     const settings = {throttlingMethod: 'simulate', throttling: {...defaultThrottling, rttMs: 300}};
     const context = {settings, computedCache: new Map()};
-    const result = await LanternSpeedIndex.request({trace, devtoolsLog, settings}, context);
+    const result = await LanternSpeedIndex.request({trace, devtoolsLog, gatherContext, settings},
+      context);
 
     expect({
       timing: Math.round(result.timing),
diff --git a/lighthouse-core/test/computed/metrics/largest-contentful-paint-all-frames-test.js b/lighthouse-core/test/computed/metrics/largest-contentful-paint-all-frames-test.js
index e3f3674dd..efdb23707 100644
--- a/lighthouse-core/test/computed/metrics/largest-contentful-paint-all-frames-test.js
+++ b/lighthouse-core/test/computed/metrics/largest-contentful-paint-all-frames-test.js
@@ -18,10 +18,12 @@ const invalidDevtoolsLog = require('../../fixtures/traces/progressive-app-m60.de
 /* eslint-env jest */
 
 describe('Metrics: LCP from all frames', () => {
+  const gatherContext = {gatherMode: 'navigation'};
+
   it('should throw for predicted value', async () => {
     const settings = {throttlingMethod: 'simulate'};
     const context = {settings, computedCache: new Map()};
-    const resultPromise = LargestContentfulPaintAllFrames.request({trace: traceAllFrames, devtoolsLog: devtoolsLogAllFrames, settings}, context); // eslint-disable-line max-len
+    const resultPromise = LargestContentfulPaintAllFrames.request({gatherContext, trace: traceAllFrames, devtoolsLog: devtoolsLogAllFrames, settings}, context); // eslint-disable-line max-len
 
     // TODO: Implement lantern solution for LCP all frames.
     expect(resultPromise).rejects.toThrow();
@@ -30,7 +32,7 @@ describe('Metrics: LCP from all frames', () => {
   it('should compute an observed value', async () => {
     const settings = {throttlingMethod: 'provided'};
     const context = {settings, computedCache: new Map()};
-    const result = await LargestContentfulPaintAllFrames.request({trace: traceAllFrames, devtoolsLog: devtoolsLogAllFrames, settings}, context); // eslint-disable-line max-len
+    const result = await LargestContentfulPaintAllFrames.request({gatherContext, trace: traceAllFrames, devtoolsLog: devtoolsLogAllFrames, settings}, context); // eslint-disable-line max-len
 
     assert.equal(Math.round(result.timing), 683);
     assert.equal(result.timestamp, 23466705983);
@@ -40,7 +42,7 @@ describe('Metrics: LCP from all frames', () => {
     const settings = {throttlingMethod: 'provided'};
     const context = {settings, computedCache: new Map()};
     const resultPromise = LargestContentfulPaintAllFrames.request(
-      {trace: invalidTrace, devtoolsLog: invalidDevtoolsLog, settings},
+      {gatherContext, trace: invalidTrace, devtoolsLog: invalidDevtoolsLog, settings},
       context
     );
     await expect(resultPromise).rejects.toThrow('NO_LCP_ALL_FRAMES');
@@ -50,7 +52,7 @@ describe('Metrics: LCP from all frames', () => {
     const settings = {throttlingMethod: 'provided'};
     const context = {settings, computedCache: new Map()};
     const result = await LargestContentfulPaintAllFrames.request(
-      {trace: traceMainFrame, devtoolsLog: devtoolsLogMainFrame, settings},
+      {gatherContext, trace: traceMainFrame, devtoolsLog: devtoolsLogMainFrame, settings},
       context
     );
     await expect(result).toEqual({
diff --git a/lighthouse-core/test/computed/metrics/largest-contentful-paint-test.js b/lighthouse-core/test/computed/metrics/largest-contentful-paint-test.js
index e5cb9638d..435e3b24f 100644
--- a/lighthouse-core/test/computed/metrics/largest-contentful-paint-test.js
+++ b/lighthouse-core/test/computed/metrics/largest-contentful-paint-test.js
@@ -16,10 +16,13 @@ const invalidDevtoolsLog = require('../../fixtures/traces/progressive-app-m60.de
 /* eslint-env jest */
 
 describe('Metrics: LCP', () => {
+  const gatherContext = {gatherMode: 'navigation'};
+
   it('should compute predicted value', async () => {
     const settings = {throttlingMethod: 'simulate'};
     const context = {settings, computedCache: new Map()};
-    const result = await LargestContentfulPaint.request({trace, devtoolsLog, settings}, context);
+    const result = await LargestContentfulPaint.request({trace, devtoolsLog, gatherContext,
+      settings}, context);
 
     expect({
       timing: Math.round(result.timing),
@@ -37,7 +40,8 @@ describe('Metrics: LCP', () => {
   it('should compute an observed value', async () => {
     const settings = {throttlingMethod: 'provided'};
     const context = {settings, computedCache: new Map()};
-    const result = await LargestContentfulPaint.request({trace, devtoolsLog, settings}, context);
+    const result = await LargestContentfulPaint.request({trace, devtoolsLog, gatherContext,
+      settings}, context);
 
     assert.equal(Math.round(result.timing), 1122);
     assert.equal(result.timestamp, 713038144775);
@@ -47,7 +51,7 @@ describe('Metrics: LCP', () => {
     const settings = {throttlingMethod: 'provided'};
     const context = {settings, computedCache: new Map()};
     const resultPromise = LargestContentfulPaint.request(
-      {trace: invalidTrace, devtoolsLog: invalidDevtoolsLog, settings},
+      {gatherContext, trace: invalidTrace, devtoolsLog: invalidDevtoolsLog, settings},
       context
     );
     await expect(resultPromise).rejects.toThrow('NO_LCP');
diff --git a/lighthouse-core/test/computed/metrics/max-potential-fid-test.js b/lighthouse-core/test/computed/metrics/max-potential-fid-test.js
index d8daefc08..6b188b44c 100644
--- a/lighthouse-core/test/computed/metrics/max-potential-fid-test.js
+++ b/lighthouse-core/test/computed/metrics/max-potential-fid-test.js
@@ -14,10 +14,13 @@ const devtoolsLog = require('../../fixtures/traces/progressive-app-m60.devtools.
 /* eslint-env jest */
 
 describe('Metrics: Max Potential FID', () => {
+  const gatherContext = {gatherMode: 'navigation'};
+
   it('should compute a simulated value', async () => {
     const settings = {throttlingMethod: 'simulate'};
     const context = {settings, computedCache: new Map()};
-    const result = await MaxPotentialFID.request({trace, devtoolsLog, settings}, context);
+    const result = await MaxPotentialFID.request({trace, devtoolsLog, gatherContext, settings},
+      context);
 
     expect({
       timing: Math.round(result.timing),
@@ -29,7 +32,8 @@ describe('Metrics: Max Potential FID', () => {
   it('should compute an observed value', async () => {
     const settings = {throttlingMethod: 'provided'};
     const context = {settings, computedCache: new Map()};
-    const result = await MaxPotentialFID.request({trace, devtoolsLog, settings}, context);
+    const result = await MaxPotentialFID.request({trace, devtoolsLog, gatherContext, settings},
+      context);
 
     assert.equal(Math.round(result.timing), 198);
   });
diff --git a/lighthouse-core/test/computed/metrics/speed-index-test.js b/lighthouse-core/test/computed/metrics/speed-index-test.js
index df9e98767..768478710 100644
--- a/lighthouse-core/test/computed/metrics/speed-index-test.js
+++ b/lighthouse-core/test/computed/metrics/speed-index-test.js
@@ -16,10 +16,13 @@ const devtoolsLog1msLayout = require('../../fixtures/traces/speedindex-1ms-layou
 /* eslint-env jest */
 
 describe('Metrics: Speed Index', () => {
+  const gatherContext = {gatherMode: 'navigation'};
+
   it('should compute a simulated value', async () => {
     const settings = {throttlingMethod: 'simulate'};
     const context = {settings, computedCache: new Map()};
-    const result = await SpeedIndex.request({trace, devtoolsLog, settings}, context);
+    const result = await SpeedIndex.request({trace, devtoolsLog, gatherContext, settings},
+      context);
 
     expect({
       timing: Math.round(result.timing),
@@ -47,6 +50,7 @@ describe('Metrics: Speed Index', () => {
     const context = {settings, computedCache: new Map()};
     const result = await SpeedIndex.request(
       {
+        gatherContext,
         trace: trace1msLayout,
         devtoolsLog: devtoolsLog1msLayout,
         settings,
@@ -70,7 +74,8 @@ describe('Metrics: Speed Index', () => {
   it('should compute an observed value (desktop)', async () => {
     const settings = {throttlingMethod: 'provided', formFactor: 'desktop'};
     const context = {settings, computedCache: new Map()};
-    const result = await SpeedIndex.request({trace, devtoolsLog, settings}, context);
+    const result = await SpeedIndex.request({trace, devtoolsLog, gatherContext, settings},
+      context);
 
     assert.equal(result.timing, 605);
     assert.equal(result.timestamp, 225414777015);
@@ -79,7 +84,8 @@ describe('Metrics: Speed Index', () => {
   it('should compute an observed value (mobile)', async () => {
     const settings = {throttlingMethod: 'provided', formFactor: 'mobile'};
     const context = {settings, computedCache: new Map()};
-    const result = await SpeedIndex.request({trace, devtoolsLog, settings}, context);
+    const result = await SpeedIndex.request({trace, devtoolsLog, gatherContext, settings},
+      context);
 
     assert.equal(result.timing, 605);
     assert.equal(result.timestamp, 225414777015);
diff --git a/lighthouse-core/test/computed/metrics/timing-summary-test.js b/lighthouse-core/test/computed/metrics/timing-summary-test.js
index 2740c6336..72b81e83b 100644
--- a/lighthouse-core/test/computed/metrics/timing-summary-test.js
+++ b/lighthouse-core/test/computed/metrics/timing-summary-test.js
@@ -13,7 +13,8 @@ const devtoolsLog = require('../../fixtures/traces/frame-metrics-m90.devtools.lo
 /* eslint-env jest */
 describe('Timing summary', () => {
   it('contains the correct data', async () => {
-    const artifacts = {settings: {throttlingMethod: 'devtools'}, trace, devtoolsLog};
+    const gatherContext = {gatherMode: 'navigation'};
+    const artifacts = {settings: {throttlingMethod: 'devtools'}, trace, devtoolsLog, gatherContext};
     const context = {computedCache: new Map()};
     const result = await TimingSummary.request(artifacts, context);
 
diff --git a/lighthouse-core/test/computed/metrics/total-blocking-time-test.js b/lighthouse-core/test/computed/metrics/total-blocking-time-test.js
index 851eb677f..92ae6d18c 100644
--- a/lighthouse-core/test/computed/metrics/total-blocking-time-test.js
+++ b/lighthouse-core/test/computed/metrics/total-blocking-time-test.js
@@ -12,10 +12,13 @@ const devtoolsLog = require('../../fixtures/traces/progressive-app-m60.devtools.
 /* eslint-env jest */
 
 describe('Metrics: TotalBlockingTime', () => {
+  const gatherContext = {gatherMode: 'navigation'};
+
   it('should compute a simulated value', async () => {
     const settings = {throttlingMethod: 'simulate'};
     const context = {settings, computedCache: new Map()};
-    const result = await TotalBlockingTime.request({trace, devtoolsLog, settings}, context);
+    const result = await TotalBlockingTime.request({trace, devtoolsLog, gatherContext, settings},
+      context);
 
     expect({
       timing: Math.round(result.timing),
@@ -33,7 +36,8 @@ describe('Metrics: TotalBlockingTime', () => {
   it('should compute an observed value', async () => {
     const settings = {throttlingMethod: 'provided'};
     const context = {settings, computedCache: new Map()};
-    const result = await TotalBlockingTime.request({trace, devtoolsLog, settings}, context);
+    const result = await TotalBlockingTime.request({trace, devtoolsLog, gatherContext, settings},
+      context);
     expect(result.timing).toBeCloseTo(48.3, 1);
   });
 
diff --git a/lighthouse-core/test/computed/page-dependency-graph-test.js b/lighthouse-core/test/computed/page-dependency-graph-test.js
index a2a2a46a4..84b78f436 100644
--- a/lighthouse-core/test/computed/page-dependency-graph-test.js
+++ b/lighthouse-core/test/computed/page-dependency-graph-test.js
@@ -30,7 +30,7 @@ const TOPLEVEL_TASK_NAME = 'TaskQueueManager::ProcessTaskFromWorkQueue';
 
 /* eslint-env jest */
 describe('PageDependencyGraph computed artifact:', () => {
-  let traceOfTab;
+  let processedTrace;
 
   function addTaskEvents(startTs, duration, evts) {
     const mainEvent = {
@@ -41,12 +41,12 @@ describe('PageDependencyGraph computed artifact:', () => {
       args: {},
     };
 
-    traceOfTab.mainThreadEvents.push(mainEvent);
+    processedTrace.mainThreadEvents.push(mainEvent);
 
     let i = 0;
     for (const evt of evts) {
       i++;
-      traceOfTab.mainThreadEvents.push({
+      processedTrace.mainThreadEvents.push({
         name: evt.name,
         ts: (evt.ts * 1000) || (startTs * 1000 + i),
         args: {data: evt.data},
@@ -55,7 +55,7 @@ describe('PageDependencyGraph computed artifact:', () => {
   }
 
   beforeEach(() => {
-    traceOfTab = {mainThreadEvents: []};
+    processedTrace = {mainThreadEvents: []};
   });
 
   describe('#compute_', () => {
@@ -142,21 +142,21 @@ describe('PageDependencyGraph computed artifact:', () => {
         {name: 'LaterEvent'},
       ]);
 
-      assert.equal(traceOfTab.mainThreadEvents.length, 7);
-      const nodes = PageDependencyGraph.getCPUNodes(traceOfTab);
+      assert.equal(processedTrace.mainThreadEvents.length, 7);
+      const nodes = PageDependencyGraph.getCPUNodes(processedTrace);
       assert.equal(nodes.length, 2);
 
       const node1 = nodes[0];
       assert.equal(node1.id, '1.0');
       assert.equal(node1.type, 'cpu');
-      assert.equal(node1.event, traceOfTab.mainThreadEvents[0]);
+      assert.equal(node1.event, processedTrace.mainThreadEvents[0]);
       assert.equal(node1.childEvents.length, 2);
       assert.equal(node1.childEvents[1].name, 'OtherEvent');
 
       const node2 = nodes[1];
       assert.equal(node2.id, '1.250000');
       assert.equal(node2.type, 'cpu');
-      assert.equal(node2.event, traceOfTab.mainThreadEvents[5]);
+      assert.equal(node2.event, processedTrace.mainThreadEvents[5]);
       assert.equal(node2.childEvents.length, 1);
       assert.equal(node2.childEvents[0].name, 'LaterEvent');
     });
@@ -172,7 +172,7 @@ describe('PageDependencyGraph computed artifact:', () => {
 
       addTaskEvents(0, 0, []);
 
-      const graph = PageDependencyGraph.createGraph(traceOfTab, networkRecords);
+      const graph = PageDependencyGraph.createGraph(processedTrace, networkRecords);
       const nodes = [];
       graph.traverse(node => nodes.push(node));
 
@@ -201,7 +201,7 @@ describe('PageDependencyGraph computed artifact:', () => {
         {name: 'XHRReadyStateChange', data: {readyState: 4, url: '4'}},
       ]);
 
-      const graph = PageDependencyGraph.createGraph(traceOfTab, networkRecords);
+      const graph = PageDependencyGraph.createGraph(processedTrace, networkRecords);
       const nodes = [];
       graph.traverse(node => nodes.push(node));
 
@@ -227,7 +227,7 @@ describe('PageDependencyGraph computed artifact:', () => {
 
       addTaskEvents(0, 0, []);
 
-      const graph = PageDependencyGraph.createGraph(traceOfTab, networkRecords);
+      const graph = PageDependencyGraph.createGraph(processedTrace, networkRecords);
       const nodes = [];
       graph.traverse(node => nodes.push(node));
 
@@ -266,7 +266,7 @@ describe('PageDependencyGraph computed artifact:', () => {
         {name: 'ResourceSendRequest', data: {requestId: 5}},
       ]);
 
-      const graph = PageDependencyGraph.createGraph(traceOfTab, networkRecords);
+      const graph = PageDependencyGraph.createGraph(processedTrace, networkRecords);
       const nodes = [];
       graph.traverse(node => nodes.push(node));
 
@@ -294,7 +294,7 @@ describe('PageDependencyGraph computed artifact:', () => {
         {name: 'TimerFire', data: {timerId: 'timer1'}},
       ]);
 
-      const graph = PageDependencyGraph.createGraph(traceOfTab, networkRecords);
+      const graph = PageDependencyGraph.createGraph(processedTrace, networkRecords);
       const nodes = [];
       graph.traverse(node => nodes.push(node));
 
@@ -327,7 +327,7 @@ describe('PageDependencyGraph computed artifact:', () => {
         {name: 'XHRReadyStateChange', data: {readyState: 4, url: '4'}},
       ]);
 
-      const graph = PageDependencyGraph.createGraph(traceOfTab, networkRecords);
+      const graph = PageDependencyGraph.createGraph(processedTrace, networkRecords);
       const nodes = [];
       graph.traverse(node => nodes.push(node));
 
@@ -372,7 +372,7 @@ describe('PageDependencyGraph computed artifact:', () => {
         {name: 'XHRReadyStateChange', data: {readyState: 4, url: '4'}},
       ]);
 
-      const graph = PageDependencyGraph.createGraph(traceOfTab, networkRecords);
+      const graph = PageDependencyGraph.createGraph(processedTrace, networkRecords);
       const nodes = [];
       graph.traverse(node => nodes.push(node));
 
@@ -395,7 +395,7 @@ describe('PageDependencyGraph computed artifact:', () => {
       const networkRecords = [request0];
 
       const makeShortEvent = firstEventName => {
-        const startTs = traceOfTab.mainThreadEvents.length * 100;
+        const startTs = processedTrace.mainThreadEvents.length * 100;
         addTaskEvents(startTs, 5, [
           {name: firstEventName, data: {url: '0'}},
         ]);
@@ -411,7 +411,7 @@ describe('PageDependencyGraph computed artifact:', () => {
         makeShortEvent(eventName);
       }
 
-      const graph = PageDependencyGraph.createGraph(traceOfTab, networkRecords);
+      const graph = PageDependencyGraph.createGraph(processedTrace, networkRecords);
       const cpuNodes = [];
       graph.traverse(node => node.type === 'cpu' && cpuNodes.push(node));
 
@@ -447,7 +447,7 @@ describe('PageDependencyGraph computed artifact:', () => {
 
       addTaskEvents(0, 0, []);
 
-      const graph = PageDependencyGraph.createGraph(traceOfTab, networkRecords);
+      const graph = PageDependencyGraph.createGraph(processedTrace, networkRecords);
       const nodes = [];
       graph.traverse(node => nodes.push(node));
 
@@ -475,7 +475,7 @@ describe('PageDependencyGraph computed artifact:', () => {
 
       addTaskEvents(0, 0, []);
 
-      const graph = PageDependencyGraph.createGraph(traceOfTab, networkRecords);
+      const graph = PageDependencyGraph.createGraph(processedTrace, networkRecords);
       const nodes = [];
       graph.traverse(node => nodes.push(node));
 
@@ -500,7 +500,7 @@ describe('PageDependencyGraph computed artifact:', () => {
 
       addTaskEvents(0, 0, []);
 
-      const graph = PageDependencyGraph.createGraph(traceOfTab, networkRecords);
+      const graph = PageDependencyGraph.createGraph(processedTrace, networkRecords);
       const nodes = [];
       graph.traverse(node => nodes.push(node));
 
@@ -528,7 +528,7 @@ describe('PageDependencyGraph computed artifact:', () => {
 
       addTaskEvents(0, 0, []);
 
-      const graph = PageDependencyGraph.createGraph(traceOfTab, networkRecords);
+      const graph = PageDependencyGraph.createGraph(processedTrace, networkRecords);
       const nodes = [];
       graph.traverse(node => nodes.push(node));
 
@@ -551,7 +551,7 @@ describe('PageDependencyGraph computed artifact:', () => {
 
       addTaskEvents(0, 0, []);
 
-      const graph = PageDependencyGraph.createGraph(traceOfTab, networkRecords);
+      const graph = PageDependencyGraph.createGraph(processedTrace, networkRecords);
       const nodes = [];
       graph.traverse(node => nodes.push(node));
       nodes.sort((a, b) => a.id - b.id);
@@ -580,7 +580,7 @@ describe('PageDependencyGraph computed artifact:', () => {
 
       addTaskEvents(0, 0, []);
 
-      const graph = PageDependencyGraph.createGraph(traceOfTab, networkRecords);
+      const graph = PageDependencyGraph.createGraph(processedTrace, networkRecords);
       const nodes = [];
       graph.traverse(node => nodes.push(node));
       nodes.sort((a, b) => a.id - b.id);
@@ -600,7 +600,7 @@ describe('PageDependencyGraph computed artifact:', () => {
 
       addTaskEvents(0, 0, []);
 
-      const fn = () => PageDependencyGraph.createGraph(traceOfTab, networkRecords);
+      const fn = () => PageDependencyGraph.createGraph(processedTrace, networkRecords);
       expect(fn).toThrow(/root node.*document/i);
     });
   });
diff --git a/lighthouse-core/test/computed/processed-navigation-test.js b/lighthouse-core/test/computed/processed-navigation-test.js
new file mode 100644
index 000000000..d76567ada
--- /dev/null
+++ b/lighthouse-core/test/computed/processed-navigation-test.js
@@ -0,0 +1,142 @@
+/**
+ * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+'use strict';
+
+/* eslint-env jest */
+
+const ProcessedTrace = require('../../computed/processed-trace.js');
+const ProcessedNavigation = require('../../computed/processed-navigation.js');
+const pwaTrace = require('../fixtures/traces/progressive-app-m60.json');
+const noFCPtrace = require('../fixtures/traces/airhorner_no_fcp.json');
+const noNavStartTrace = require('../fixtures/traces/no_navstart_event.json');
+
+describe('ProcessedTrace', () => {
+  it('computes the artifact', async () => {
+    const context = {computedCache: new Map()};
+    const processedTrace = await ProcessedTrace.request(pwaTrace, context);
+    const processedNavigation = await ProcessedNavigation.request(processedTrace, context);
+
+    expect(processedNavigation).toEqual({
+      domContentLoadedEvt: {
+        args: {
+          frame: '0x25a638821e30',
+        },
+        cat: 'blink.user_timing,rail',
+        name: 'domContentLoadedEventEnd',
+        ph: 'R',
+        pid: 44277,
+        tid: 775,
+        ts: 225414732309,
+        tts: 924831,
+      },
+      firstContentfulPaintEvt: {
+        args: {
+          frame: '0x25a638821e30',
+        },
+        cat: 'loading,rail,devtools.timeline',
+        name: 'firstContentfulPaint',
+        ph: 'I',
+        pid: 44277,
+        s: 'p',
+        tid: 775,
+        ts: 225414670885,
+        tts: 866570,
+      },
+      firstContentfulPaintAllFramesEvt: {
+        args: {
+          frame: '0x25a638821e30',
+        },
+        cat: 'loading,rail,devtools.timeline',
+        name: 'firstContentfulPaint',
+        ph: 'I',
+        pid: 44277,
+        s: 'p',
+        tid: 775,
+        ts: 225414670885,
+        tts: 866570,
+      },
+      firstMeaningfulPaintEvt: {
+        args: {
+          frame: '0x25a638821e30',
+        },
+        cat: 'loading,rail,devtools.timeline',
+        name: 'firstMeaningfulPaint',
+        ph: 'R',
+        pid: 44277,
+        tid: 775,
+        ts: 225414955343,
+        tts: 2676979,
+      },
+      firstPaintEvt: {
+        args: {
+          frame: '0x25a638821e30',
+        },
+        cat: 'loading,rail,devtools.timeline',
+        name: 'firstPaint',
+        ph: 'I',
+        pid: 44277,
+        s: 'p',
+        tid: 775,
+        ts: 225414670868,
+        tts: 866553,
+      },
+      fmpFellBack: false,
+      lcpInvalidated: false,
+      loadEvt: {
+        args: {
+          frame: '0x25a638821e30',
+        },
+        cat: 'blink.user_timing',
+        name: 'loadEventEnd',
+        ph: 'R',
+        pid: 44277,
+        tid: 775,
+        ts: 225416370913,
+        tts: 2369379,
+      },
+      timestamps: {
+        domContentLoaded: 225414732309,
+        firstContentfulPaint: 225414670885,
+        firstContentfulPaintAllFrames: 225414670885,
+        firstMeaningfulPaint: 225414955343,
+        firstPaint: 225414670868,
+        load: 225416370913,
+        timeOrigin: 225414172015,
+        traceEnd: 225426711887,
+      },
+      timings: {
+        domContentLoaded: 560.294,
+        firstContentfulPaint: 498.87,
+        firstContentfulPaintAllFrames: 498.87,
+        firstMeaningfulPaint: 783.328,
+        firstPaint: 498.853,
+        load: 2198.898,
+        timeOrigin: 0,
+        traceEnd: 12539.872,
+      },
+    });
+  });
+
+  it('fails with NO_NAVSTART', async () => {
+    const context = {computedCache: new Map()};
+    const compute = async () => {
+      const processedTrace = await ProcessedTrace.request(noNavStartTrace, context);
+      await ProcessedNavigation.request(processedTrace, context);
+    };
+    await expect(compute()).rejects.toMatchObject({code: 'NO_NAVSTART'});
+  });
+
+  it('fails with NO_FCP', async () => {
+    const context = {computedCache: new Map()};
+
+    const compute = async () => {
+      const processedTrace = await ProcessedTrace.request(noFCPtrace, context);
+      await ProcessedNavigation.request(processedTrace, context);
+    };
+
+    await expect(compute()).rejects.toMatchObject({code: 'NO_FCP'});
+  });
+});
diff --git a/lighthouse-core/test/computed/processed-trace-test.js b/lighthouse-core/test/computed/processed-trace-test.js
new file mode 100644
index 000000000..5112b3ee1
--- /dev/null
+++ b/lighthouse-core/test/computed/processed-trace-test.js
@@ -0,0 +1,73 @@
+/**
+ * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+ */
+'use strict';
+
+/* eslint-env jest */
+
+const ProcessedTrace = require('../../computed/processed-trace.js');
+const pwaTrace = require('../fixtures/traces/progressive-app-m60.json');
+
+describe('ProcessedTrace', () => {
+  it('computes the artifact', async () => {
+    const context = {computedCache: new Map()};
+    const processedTrace = await ProcessedTrace.request(pwaTrace, context);
+
+    expect(processedTrace.processEvents.length).toEqual(12865);
+    expect(processedTrace.mainThreadEvents.length).toEqual(7629);
+
+    delete processedTrace.processEvents;
+    delete processedTrace.mainThreadEvents;
+    delete processedTrace.frameTreeEvents;
+    delete processedTrace.frameEvents;
+
+    expect(processedTrace).toEqual({
+      mainFrameIds: {
+        frameId: '0x25a638821e30',
+        pid: 44277,
+        tid: 775,
+      },
+      timeOriginEvt: {
+        args: {
+          frame: '0x25a638821e30',
+        },
+        cat: 'blink.user_timing',
+        name: 'navigationStart',
+        ph: 'R',
+        pid: 44277,
+        tid: 775,
+        ts: 225414172015,
+        tts: 455539,
+      },
+      frames: [],
+      timestamps: {
+        timeOrigin: 225414172015,
+        traceEnd: 225426711887,
+      },
+      timings: {
+        timeOrigin: 0,
+        traceEnd: 12539.872,
+      },
+    });
+  });
+
+  it('fails with NO_TRACING_STARTED', async () => {
+    const context = {computedCache: new Map()};
+    const noTracingStartedTrace = {
+      traceEvents: pwaTrace.traceEvents.filter(event => {
+        if (event.name === 'TracingStartedInBrowser' ||
+            event.name === 'TracingStartedInPage' ||
+            event.name === 'ResourceSendRequest') {
+          return false;
+        }
+
+        return true;
+      }),
+    };
+
+    await expect(ProcessedTrace.request(noTracingStartedTrace, context))
+      .rejects.toMatchObject({code: 'NO_TRACING_STARTED'});
+  });
+});
diff --git a/lighthouse-core/test/computed/trace-of-tab-test.js b/lighthouse-core/test/computed/trace-of-tab-test.js
index 53c5dc754..98c322a67 100644
--- a/lighthouse-core/test/computed/trace-of-tab-test.js
+++ b/lighthouse-core/test/computed/trace-of-tab-test.js
@@ -9,10 +9,8 @@
 
 const TraceOfTab = require('../../computed/trace-of-tab.js');
 const pwaTrace = require('../fixtures/traces/progressive-app-m60.json');
-const noFCPtrace = require('../fixtures/traces/airhorner_no_fcp.json');
-const noNavStartTrace = require('../fixtures/traces/no_navstart_event.json');
 
-describe('TraceOfTabComputed', () => {
+describe('TraceOfTab', () => {
   it('computes the artifact', async () => {
     const context = {computedCache: new Map()};
     const traceOfTab = await TraceOfTab.request(pwaTrace, context);
@@ -23,8 +21,27 @@ describe('TraceOfTabComputed', () => {
     delete traceOfTab.processEvents;
     delete traceOfTab.mainThreadEvents;
     delete traceOfTab.frameTreeEvents;
+    delete traceOfTab.frameEvents;
 
     expect(traceOfTab).toEqual({
+      mainFrameIds: {
+        frameId: '0x25a638821e30',
+        pid: 44277,
+        tid: 775,
+      },
+      timeOriginEvt: {
+        args: {
+          frame: '0x25a638821e30',
+        },
+        cat: 'blink.user_timing',
+        name: 'navigationStart',
+        ph: 'R',
+        pid: 44277,
+        tid: 775,
+        ts: 225414172015,
+        tts: 455539,
+      },
+      frames: [],
       domContentLoadedEvt: {
         args: {
           frame: '0x25a638821e30',
@@ -102,24 +119,6 @@ describe('TraceOfTabComputed', () => {
         ts: 225416370913,
         tts: 2369379,
       },
-      mainFrameIds: {
-        frameId: '0x25a638821e30',
-        pid: 44277,
-        tid: 775,
-      },
-      timeOriginEvt: {
-        args: {
-          frame: '0x25a638821e30',
-        },
-        cat: 'blink.user_timing',
-        name: 'navigationStart',
-        ph: 'R',
-        pid: 44277,
-        tid: 775,
-        ts: 225414172015,
-        tts: 455539,
-      },
-      frames: [],
       timestamps: {
         domContentLoaded: 225414732309,
         firstContentfulPaint: 225414670885,
@@ -142,34 +141,4 @@ describe('TraceOfTabComputed', () => {
       },
     });
   });
-
-  it('fails with NO_NAVSTART', async () => {
-    const context = {computedCache: new Map()};
-    await expect(TraceOfTab.request(noNavStartTrace, context))
-      .rejects.toMatchObject({code: 'NO_NAVSTART'});
-  });
-
-  it('fails with NO_FCP', async () => {
-    const context = {computedCache: new Map()};
-    await expect(TraceOfTab.request(noFCPtrace, context))
-      .rejects.toMatchObject({code: 'NO_FCP'});
-  });
-
-  it('fails with NO_TRACING_STARTED', async () => {
-    const context = {computedCache: new Map()};
-    const noTracingStartedTrace = {
-      traceEvents: pwaTrace.traceEvents.filter(event => {
-        if (event.name === 'TracingStartedInBrowser' ||
-            event.name === 'TracingStartedInPage' ||
-            event.name === 'ResourceSendRequest') {
-          return false;
-        }
-
-        return true;
-      }),
-    };
-
-    await expect(TraceOfTab.request(noTracingStartedTrace, context))
-      .rejects.toMatchObject({code: 'NO_TRACING_STARTED'});
-  });
 });
diff --git a/lighthouse-core/test/config/config-test.js b/lighthouse-core/test/config/config-test.js
index 1c8f3061d..3d8b473af 100644
--- a/lighthouse-core/test/config/config-test.js
+++ b/lighthouse-core/test/config/config-test.js
@@ -538,7 +538,7 @@ describe('Config', () => {
         onlyAudits: ['color-contrast'],
       },
       passes: [
-        {recordTrace: true, gatherers: []},
+        {recordTrace: true, gatherers: ['gather-context']},
         {passName: 'a11y', gatherers: ['accessibility']},
       ],
       audits: [
@@ -582,7 +582,7 @@ describe('Config', () => {
         skipAudits: ['first-meaningful-paint'],
       },
       passes: [
-        {recordTrace: true, gatherers: []},
+        {recordTrace: true, gatherers: ['gather-context']},
         {passName: 'a11y', gatherers: ['accessibility']},
       ],
       audits: [
diff --git a/lighthouse-core/test/fraggle-rock/config/filters-test.js b/lighthouse-core/test/fraggle-rock/config/filters-test.js
index df82b4061..107b2b1fd 100644
--- a/lighthouse-core/test/fraggle-rock/config/filters-test.js
+++ b/lighthouse-core/test/fraggle-rock/config/filters-test.js
@@ -69,6 +69,14 @@ describe('Fraggle Rock Config Filtering', () => {
       ...auditMeta,
     };
   }
+  class NavigationOnlyAudit extends BaseAudit {
+    static meta = {
+      id: 'navigation-only',
+      requiredArtifacts: /** @type {any} */ (['Snapshot', 'Timespan']),
+      supportedModes: /** @type {['navigation']} */ (['navigation']),
+      ...auditMeta,
+    };
+  }
 
   const audits = [SnapshotAudit, TimespanAudit, NavigationAudit, ManualAudit].map(audit => ({
     implementation: audit,
@@ -180,6 +188,33 @@ describe('Fraggle Rock Config Filtering', () => {
     });
   });
 
+  describe('filterAuditsByGatherMode', () => {
+    it('should handle null', () => {
+      expect(filters.filterAuditsByGatherMode(null, 'timespan')).toBeNull();
+    });
+
+    it('should filter unsupported audits', () => {
+      const timespanAudits = [TimespanAudit, NavigationOnlyAudit].map(audit => ({
+        implementation: audit,
+        options: {},
+      }));
+      expect(filters.filterAuditsByGatherMode(timespanAudits, 'timespan')).toEqual([
+        {implementation: TimespanAudit, options: {}},
+      ]);
+    });
+
+    it('should keep audits without explicit modes defined', () => {
+      const timespanAudits = [TimespanAudit, NavigationAudit].map(audit => ({
+        implementation: audit,
+        options: {},
+      }));
+      expect(filters.filterAuditsByGatherMode(timespanAudits, 'timespan')).toEqual([
+        {implementation: TimespanAudit, options: {}},
+        {implementation: NavigationAudit, options: {}},
+      ]);
+    });
+  });
+
   describe('filterConfigByGatherMode', () => {
     it('should filter the entire config', () => {
       const config = {
diff --git a/lighthouse-core/test/gather/gatherers/trace-elements-test.js b/lighthouse-core/test/gather/gatherers/trace-elements-test.js
index 629a7cc2b..3119c5429 100644
--- a/lighthouse-core/test/gather/gatherers/trace-elements-test.js
+++ b/lighthouse-core/test/gather/gatherers/trace-elements-test.js
@@ -491,7 +491,7 @@ describe('Trace Elements gatherer - Animated Elements', () => {
     const gatherer = new TraceElementsGatherer();
     gatherer.animationIdToName.set('1', 'example');
 
-    const result = await gatherer._getArtifact({driver}, trace);
+    const result = await gatherer._getArtifact({driver, computedCache: new Map()}, trace);
 
     expect(result).toEqual([
       {
@@ -577,7 +577,7 @@ describe('Trace Elements gatherer - Animated Elements', () => {
     gatherer.animationIdToName.set('3', 'beta');
     gatherer.animationIdToName.set('4', 'gamma');
 
-    const result = await gatherer._getArtifact({driver}, animationTrace);
+    const result = await gatherer._getArtifact({driver, computedCache: new Map()}, animationTrace);
 
     expect(result).toEqual([
       {
@@ -664,7 +664,7 @@ describe('Trace Elements gatherer - Animated Elements', () => {
     gatherer.animationIdToName.set('1', 'notgunnamatter');
     gatherer.animationIdToName.set('2', 'example');
 
-    const result = await gatherer._getArtifact({driver}, trace);
+    const result = await gatherer._getArtifact({driver, computedCache: new Map()}, trace);
 
     expect(result).toEqual([
       {
@@ -695,11 +695,11 @@ describe('instrumentation', () => {
       .mockResponse('Animation.disable');
     const driver = new Driver(connectionStub);
     const gatherer = new TraceElementsGatherer();
-    await gatherer.startInstrumentation({driver});
+    await gatherer.startInstrumentation({driver, computedCache: new Map()});
 
     await flushAllTimersAndMicrotasks();
 
-    await gatherer.stopInstrumentation({driver});
+    await gatherer.stopInstrumentation({driver, computedCache: new Map()});
 
     expect(gatherer.animationIdToName.size).toEqual(1);
     expect(gatherer.animationIdToName.get('1')).toEqual('example');
@@ -717,11 +717,11 @@ describe('instrumentation', () => {
       .mockResponse('Animation.disable');
     const driver = new Driver(connectionStub);
     const gatherer = new TraceElementsGatherer();
-    await gatherer.startInstrumentation({driver});
+    await gatherer.startInstrumentation({driver, computedCache: new Map()});
 
     await flushAllTimersAndMicrotasks();
 
-    await gatherer.stopInstrumentation({driver});
+    await gatherer.stopInstrumentation({driver, computedCache: new Map()});
 
     expect(gatherer.animationIdToName.size).toEqual(0);
   });
diff --git a/lighthouse-core/test/lib/minify-devtoolslog-test.js b/lighthouse-core/test/lib/minify-devtoolslog-test.js
index b3df5f9e8..a7c0d2bfd 100644
--- a/lighthouse-core/test/lib/minify-devtoolslog-test.js
+++ b/lighthouse-core/test/lib/minify-devtoolslog-test.js
@@ -14,7 +14,11 @@ const MetricsAudit = require('../../audits/metrics.js');
 
 describe('minify-devtoolslog', () => {
   it('has identical metrics to unminified', async () => {
-    const artifacts = {traces: {defaultPass: trace}, devtoolsLogs: {defaultPass: devtoolsLog}};
+    const artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
+      traces: {defaultPass: trace},
+      devtoolsLogs: {defaultPass: devtoolsLog},
+    };
     const context = {settings: {throttlingMethod: 'simulate'}, computedCache: new Map()};
     const {details: {items: [before]}} = await MetricsAudit.audit(artifacts, context);
     const beforeSize = JSON.stringify(devtoolsLog).length;
diff --git a/lighthouse-core/test/lib/minify-trace-test.js b/lighthouse-core/test/lib/minify-trace-test.js
index a7ff046c2..5830ff339 100644
--- a/lighthouse-core/test/lib/minify-trace-test.js
+++ b/lighthouse-core/test/lib/minify-trace-test.js
@@ -14,7 +14,11 @@ const MetricsAudit = require('../../audits/metrics.js');
 
 describe('minify-trace', () => {
   it('has identical metrics to unminified', async () => {
-    const artifacts = {traces: {defaultPass: trace}, devtoolsLogs: {defaultPass: devtoolsLog}};
+    const artifacts = {
+      GatherContext: {gatherMode: 'navigation'},
+      traces: {defaultPass: trace},
+      devtoolsLogs: {defaultPass: devtoolsLog},
+    };
     const context = {settings: {throttlingMethod: 'simulate'}, computedCache: new Map()};
     const {details: {items: [before]}} = await MetricsAudit.audit(artifacts, context);
     const beforeSize = JSON.stringify(trace).length;
diff --git a/lighthouse-core/test/lib/tracehouse/cpu-profile-model-test.js b/lighthouse-core/test/lib/tracehouse/cpu-profile-model-test.js
index f7e781e51..60442dc68 100644
--- a/lighthouse-core/test/lib/tracehouse/cpu-profile-model-test.js
+++ b/lighthouse-core/test/lib/tracehouse/cpu-profile-model-test.js
@@ -413,7 +413,7 @@ describe('CPU Profiler Model', () => {
     });
 
     it('should work on a real trace', () => {
-      const {processEvents} = TraceProcessor.computeTraceOfTab(profilerTrace);
+      const {processEvents} = TraceProcessor.processTrace(profilerTrace);
       const profiles = CpuProfileModel.collectProfileEvents(processEvents);
       const events = CpuProfileModel.synthesizeTraceEvents(profiles[0]);
       expect(events).toHaveLength(230);
@@ -472,7 +472,7 @@ describe('CPU Profiler Model', () => {
     });
 
     it('should work on a real trace', () => {
-      const {processEvents} = TraceProcessor.computeTraceOfTab(profilerTrace);
+      const {processEvents} = TraceProcessor.processTrace(profilerTrace);
       const profiles = CpuProfileModel.collectProfileEvents(processEvents);
       expect(profiles).toMatchObject([
         {
diff --git a/lighthouse-core/test/lib/tracehouse/main-thread-tasks-test.js b/lighthouse-core/test/lib/tracehouse/main-thread-tasks-test.js
index 569c8d796..089f8a9d5 100644
--- a/lighthouse-core/test/lib/tracehouse/main-thread-tasks-test.js
+++ b/lighthouse-core/test/lib/tracehouse/main-thread-tasks-test.js
@@ -32,10 +32,15 @@ describe('Main Thread Tasks', () => {
   });
 
   function run(trace) {
-    const {mainThreadEvents, frames, timestamps} = TraceProcessor.computeTraceOfTab(trace);
+    const {mainThreadEvents, frames, timestamps} = TraceProcessor.processTrace(trace);
     return MainThreadTasks.getMainThreadTasks(mainThreadEvents, frames, timestamps.traceEnd);
   }
 
+  /** @param {string} s */
+  function trimTrailingWhitespace(s) {
+    return s.split('\n').map(line => line.trimEnd()).join('\n');
+  }
+
   it('should get all main thread tasks from a trace', () => {
     const tasks = run({traceEvents: pwaTrace});
     const toplevelTasks = tasks.filter(task => !task.parent);
@@ -854,14 +859,14 @@ describe('Main Thread Tasks', () => {
 
       const tasks = run({traceEvents});
       const taskTreeAsString = MainThreadTasks.printTaskTreeToDebugString(tasks, {printWidth: 50});
-      expect(taskTreeAsString).toMatchInlineSnapshot(`
+      expect(trimTrailingWhitespace(taskTreeAsString)).toMatchInlineSnapshot(`
         "Trace Duration: 100ms
         Range: [0, 100]
         █ = 2.00ms
 
         █████████████████████████A█████████████████████████
-          ████████████B█████████████  ███████D████████    
-             ███████C████████                             
+          ████████████B█████████████  ███████D████████
+             ███████C████████
 
         A = TaskA
         B = TaskB
diff --git a/lighthouse-core/test/lib/tracehouse/trace-processor-test.js b/lighthouse-core/test/lib/tracehouse/trace-processor-test.js
index f90b5b8ac..27c26fc5a 100644
--- a/lighthouse-core/test/lib/tracehouse/trace-processor-test.js
+++ b/lighthouse-core/test/lib/tracehouse/trace-processor-test.js
@@ -198,7 +198,9 @@ describe('TraceProcessor', () => {
         ['E', 'D'],
       ]);
     });
+  });
 
+  describe('.processTrace() - frameTreeEvents', () => {
     it('frameTreeEvents excludes other frame trees', () => {
       const testTrace = createTestTrace({timeOrigin: 0, traceEnd: 2000});
       const mainFrame = testTrace.traceEvents[0].args.frame;
@@ -215,7 +217,7 @@ describe('TraceProcessor', () => {
         {name: 'Event3', cat, args: {frame: otherMainFrame}}
         /* eslint-enable max-len */
       );
-      const trace = TraceProcessor.computeTraceOfTab(testTrace);
+      const trace = TraceProcessor.processTrace(testTrace);
       expect(trace.frameTreeEvents.map(e => e.name)).toEqual([
         'navigationStart',
         'domContentLoadedEventEnd',
@@ -239,7 +241,7 @@ describe('TraceProcessor', () => {
         {name: 'Event3', cat, args: {frame: otherMainFrame}}
         /* eslint-enable max-len */
       );
-      const trace = TraceProcessor.computeTraceOfTab(testTrace);
+      const trace = TraceProcessor.processTrace(testTrace);
       expect(trace.frameTreeEvents.map(e => e.name)).toEqual([
         'navigationStart',
         'domContentLoadedEventEnd',
@@ -253,7 +255,7 @@ describe('TraceProcessor', () => {
   describe('getMainThreadTopLevelEvents', () => {
     it('gets durations of top-level tasks', () => {
       const trace = {traceEvents: pwaTrace};
-      const tabTrace = TraceProcessor.computeTraceOfTab(trace);
+      const tabTrace = TraceProcessor.processTrace(trace);
       const ret = TraceProcessor.getMainThreadTopLevelEvents(tabTrace);
 
       assert.equal(ret.length, 645);
@@ -294,7 +296,7 @@ describe('TraceProcessor', () => {
   describe('getMainThreadTopLevelEventDurations', () => {
     it('gets durations of top-level tasks', async () => {
       const trace = {traceEvents: pwaTrace};
-      const tabTrace = TraceProcessor.computeTraceOfTab(trace);
+      const tabTrace = TraceProcessor.processTrace(trace);
       const events = TraceProcessor.getMainThreadTopLevelEvents(tabTrace);
       const ret = TraceProcessor.getMainThreadTopLevelEventDurations(events);
       const durations = ret.durations;
@@ -332,7 +334,7 @@ describe('TraceProcessor', () => {
 
     it('compute correct defaults', async () => {
       const trace = {traceEvents: pwaTrace};
-      const tabTrace = TraceProcessor.computeTraceOfTab(trace);
+      const tabTrace = TraceProcessor.processTrace(trace);
       const events = TraceProcessor.getMainThreadTopLevelEvents(tabTrace);
       const ret = TraceProcessor.getRiskToResponsiveness(events, 0, tabTrace.timings.traceEnd);
       assert.equal(ret.durations.length, 645);
@@ -353,9 +355,9 @@ describe('TraceProcessor', () => {
     });
   });
 
-  describe('computeTraceOfTab', () => {
+  describe('.processTrace()', () => {
     it('gathers the events from the tab\'s process', () => {
-      const trace = TraceProcessor.computeTraceOfTab(lateTracingStartedTrace);
+      const trace = TraceProcessor.processTrace(lateTracingStartedTrace);
 
       const firstEvt = trace.processEvents[0];
       trace.processEvents.forEach(evt => {
@@ -364,32 +366,39 @@ describe('TraceProcessor', () => {
 
       assert.ok(firstEvt.pid === trace.mainFrameIds.pid);
       assert.ok(firstEvt.pid === trace.timeOriginEvt.pid);
-      assert.ok(firstEvt.pid === trace.firstContentfulPaintEvt.pid);
-      assert.ok(firstEvt.pid === trace.firstMeaningfulPaintEvt.pid);
     });
 
     it('computes timings of each event', () => {
-      const trace = TraceProcessor.computeTraceOfTab(lateTracingStartedTrace);
+      const trace = TraceProcessor.processTrace(lateTracingStartedTrace);
       assert.equal(Math.round(trace.timings.timeOrigin), 0);
-      assert.equal(Math.round(trace.timings.firstPaint), 80);
-      assert.equal(Math.round(trace.timings.firstContentfulPaint), 80);
-      assert.equal(Math.round(trace.timings.firstMeaningfulPaint), 530);
       assert.equal(Math.round(trace.timings.traceEnd), 649);
     });
 
     it('computes timestamps of each event', () => {
-      const trace = TraceProcessor.computeTraceOfTab(lateTracingStartedTrace);
+      const trace = TraceProcessor.processTrace(lateTracingStartedTrace);
       assert.equal(Math.round(trace.timestamps.timeOrigin), 29343540951);
-      assert.equal(Math.round(trace.timestamps.firstPaint), 29343620997);
-      assert.equal(Math.round(trace.timestamps.firstContentfulPaint), 29343621005);
-      assert.equal(Math.round(trace.timestamps.firstMeaningfulPaint), 29344070867);
       assert.equal(Math.round(trace.timestamps.traceEnd), 29344190232);
     });
 
+    it('sorts events by increasing timestamp', () => {
+      const trace = JSON.parse(fs.readFileSync(__dirname +
+          '/../../fixtures/traces/tracingstarted-after-navstart.json', 'utf8'));
+      const shuffledEvents = trace.traceEvents.slice().sort(() => Math.random() * 2 - 1);
+      const processedTrace = TraceProcessor.processTrace({traceEvents: shuffledEvents});
+
+      let lastTs = -Infinity;
+      for (const event of processedTrace.processEvents) {
+        if (!event.ts) continue;
+        expect(event.ts).toBeGreaterThanOrEqual(lastTs);
+        lastTs = event.ts;
+      }
+    });
+
     describe('timeOriginDeterminationMethod', () => {
       it('supports lastNavigationStart', () => {
-        const trace = TraceProcessor.computeTraceOfTab(lcpTrace);
-        expect(trace.timings).toMatchObject({
+        const trace = TraceProcessor.processTrace(lcpTrace);
+        const navigation = TraceProcessor.processNavigation(trace);
+        expect(navigation.timings).toMatchObject({
           largestContentfulPaint: 1121.711,
           load: 2159.007,
           traceEnd: 7416.038,
@@ -399,11 +408,12 @@ describe('TraceProcessor', () => {
       });
 
       it('supports firstResourceSendRequest', () => {
-        const trace = TraceProcessor.computeTraceOfTab(lcpTrace, {
+        const trace = TraceProcessor.processTrace(lcpTrace, {
           timeOriginDeterminationMethod: 'firstResourceSendRequest',
         });
+        const navigation = TraceProcessor.processNavigation(trace);
 
-        expect(trace.timings).toMatchObject({
+        expect(navigation.timings).toMatchObject({
           largestContentfulPaint: 812.683,
           load: 1849.979,
           traceEnd: 7107.01,
@@ -413,56 +423,91 @@ describe('TraceProcessor', () => {
       });
     });
 
-    describe('finds correct FMP', () => {
+    describe('.processNavigation()', () => {
+      it('gathers the events from the tab\'s process', () => {
+        const trace = TraceProcessor.processTrace(lateTracingStartedTrace);
+        const navigation = TraceProcessor.processNavigation(trace);
+
+        assert.ok(trace.mainFrameIds.pid === navigation.firstContentfulPaintEvt.pid);
+        assert.ok(trace.mainFrameIds.pid === navigation.firstMeaningfulPaintEvt.pid);
+      });
+
+      it('computes timings of each event', () => {
+        const trace = TraceProcessor.processTrace(lateTracingStartedTrace);
+        const navigation = TraceProcessor.processNavigation(trace);
+        assert.equal(Math.round(navigation.timings.timeOrigin), 0);
+        assert.equal(Math.round(navigation.timings.firstPaint), 80);
+        assert.equal(Math.round(navigation.timings.firstContentfulPaint), 80);
+        assert.equal(Math.round(navigation.timings.firstMeaningfulPaint), 530);
+        assert.equal(Math.round(navigation.timings.traceEnd), 649);
+      });
+
+      it('computes timestamps of each event', () => {
+        const trace = TraceProcessor.processTrace(lateTracingStartedTrace);
+        const navigation = TraceProcessor.processNavigation(trace);
+        assert.equal(Math.round(navigation.timestamps.timeOrigin), 29343540951);
+        assert.equal(Math.round(navigation.timestamps.firstPaint), 29343620997);
+        assert.equal(Math.round(navigation.timestamps.firstContentfulPaint), 29343621005);
+        assert.equal(Math.round(navigation.timestamps.firstMeaningfulPaint), 29344070867);
+        assert.equal(Math.round(navigation.timestamps.traceEnd), 29344190232);
+      });
+    });
+
+    describe('.processNavigation() - FMP', () => {
       it('if there was a tracingStartedInPage after the frame\'s navStart', () => {
-        const trace = TraceProcessor.computeTraceOfTab(lateTracingStartedTrace);
+        const trace = TraceProcessor.processTrace(lateTracingStartedTrace);
+        const navigation = TraceProcessor.processNavigation(trace);
         assert.equal(trace.mainFrameIds.frameId, '0x163736997740');
         assert.equal(trace.timeOriginEvt.ts, 29343540951);
-        assert.equal(trace.firstContentfulPaintEvt.ts, 29343621005);
-        assert.equal(trace.firstMeaningfulPaintEvt.ts, 29344070867);
-        assert.ok(!trace.fmpFellBack);
+        assert.equal(navigation.firstContentfulPaintEvt.ts, 29343621005);
+        assert.equal(navigation.firstMeaningfulPaintEvt.ts, 29344070867);
+        assert.ok(!navigation.fmpFellBack);
       });
 
       it('if there was a tracingStartedInPage after the frame\'s navStart #2', () => {
-        const trace = TraceProcessor.computeTraceOfTab(badNavStartTrace);
+        const trace = TraceProcessor.processTrace(badNavStartTrace);
+        const navigation = TraceProcessor.processNavigation(trace);
         assert.equal(trace.mainFrameIds.frameId, '0x89915541e48');
         assert.equal(trace.timeOriginEvt.ts, 8885424467);
-        assert.equal(trace.firstContentfulPaintEvt.ts, 8886056886);
-        assert.equal(trace.firstMeaningfulPaintEvt.ts, 8886056891);
-        assert.ok(!trace.fmpFellBack);
+        assert.equal(navigation.firstContentfulPaintEvt.ts, 8886056886);
+        assert.equal(navigation.firstMeaningfulPaintEvt.ts, 8886056891);
+        assert.ok(!navigation.fmpFellBack);
       });
 
       it('if it appears slightly before the fCP', () => {
-        const trace = TraceProcessor.computeTraceOfTab(preactTrace);
+        const trace = TraceProcessor.processTrace(preactTrace);
+        const navigation = TraceProcessor.processNavigation(trace);
         assert.equal(trace.mainFrameIds.frameId, '0x25edaa521e58');
         assert.equal(trace.timeOriginEvt.ts, 1805796384607);
-        assert.equal(trace.firstContentfulPaintEvt.ts, 1805797263653);
-        assert.equal(trace.firstMeaningfulPaintEvt.ts, 1805797262960);
-        assert.ok(!trace.fmpFellBack);
+        assert.equal(navigation.firstContentfulPaintEvt.ts, 1805797263653);
+        assert.equal(navigation.firstMeaningfulPaintEvt.ts, 1805797262960);
+        assert.ok(!navigation.fmpFellBack);
       });
 
       it('from candidates if no defined FMP exists', () => {
-        const trace = TraceProcessor.computeTraceOfTab(noFMPtrace);
+        const trace = TraceProcessor.processTrace(noFMPtrace);
+        const navigation = TraceProcessor.processNavigation(trace);
         assert.equal(trace.mainFrameIds.frameId, '0x150343381dd0');
         assert.equal(trace.timeOriginEvt.ts, 2146735807738);
-        assert.equal(trace.firstContentfulPaintEvt.ts, 2146737302468);
-        assert.equal(trace.firstMeaningfulPaintEvt.ts, 2146740268666);
-        assert.ok(trace.fmpFellBack);
+        assert.equal(navigation.firstContentfulPaintEvt.ts, 2146737302468);
+        assert.equal(navigation.firstMeaningfulPaintEvt.ts, 2146740268666);
+        assert.ok(navigation.fmpFellBack);
       });
     });
 
-    describe('finds correct LCP', () => {
+    describe('.processNavigation() - LCP', () => {
       it('in a trace', () => {
-        const trace = TraceProcessor.computeTraceOfTab(lcpTrace);
+        const trace = TraceProcessor.processTrace(lcpTrace);
+        const navigation = TraceProcessor.processNavigation(trace);
         expect({
-          'firstContentfulPaintEvt.ts': trace.firstContentfulPaintEvt.ts,
-          'largestContentfulPaintEvt.ts': trace.largestContentfulPaintEvt.ts,
+          'firstContentfulPaintEvt.ts': navigation.firstContentfulPaintEvt.ts,
+          'largestContentfulPaintEvt.ts': navigation.largestContentfulPaintEvt.ts,
           'mainFrameIds.frameId': trace.mainFrameIds.frameId,
           'timeOriginEvt.ts': trace.timeOriginEvt.ts,
-          'timestamps.firstContentfulPaint': trace.timestamps.firstContentfulPaint,
-          'timestamps.largestContentfulPaint': trace.timestamps.largestContentfulPaint,
-          'timings.firstContentfulPaint': trace.timings.firstContentfulPaint,
-          'timings.largestContentfulPaint': trace.timings.largestContentfulPaint,
+          'timestamps.firstContentfulPaint': navigation.timestamps.firstContentfulPaint,
+          'timestamps.largestContentfulPaint': navigation.timestamps.largestContentfulPaint,
+          'timings.firstContentfulPaint': navigation.timings.firstContentfulPaint,
+          'timings.largestContentfulPaint': navigation.timings.largestContentfulPaint,
         }).toMatchInlineSnapshot(`
 Object {
   "firstContentfulPaintEvt.ts": 713038144775,
@@ -475,7 +520,7 @@ Object {
   "timings.largestContentfulPaint": 1121.711,
 }
 `);
-        assert.ok(!trace.lcpInvalidated);
+        assert.ok(!navigation.lcpInvalidated);
       });
 
       it('uses latest candidate', () => {
@@ -488,16 +533,18 @@ Object {
           {name: 'largestContentfulPaint::Invalidate', cat, args, ts: 1100, duration: 10},
           {name: 'largestContentfulPaint::Candidate', cat, args, ts: 1200, duration: 10}
         );
-        const trace = TraceProcessor.computeTraceOfTab(testTrace);
-        assert.equal(trace.timestamps.largestContentfulPaint, 1200);
-        assert.ok(!trace.lcpInvalidated);
+        const trace = TraceProcessor.processTrace(testTrace);
+        const navigation = TraceProcessor.processNavigation(trace);
+        assert.equal(navigation.timestamps.largestContentfulPaint, 1200);
+        assert.ok(!navigation.lcpInvalidated);
       });
 
       it('undefined if no candidates', () => {
         const testTrace = createTestTrace({timeOrigin: 0, traceEnd: 2000});
-        const trace = TraceProcessor.computeTraceOfTab(testTrace);
-        assert.equal(trace.timestamps.largestContentfulPaint, undefined);
-        assert.ok(!trace.lcpInvalidated);
+        const trace = TraceProcessor.processTrace(testTrace);
+        const navigation = TraceProcessor.processNavigation(trace);
+        assert.equal(navigation.timestamps.largestContentfulPaint, undefined);
+        assert.ok(!navigation.lcpInvalidated);
       });
 
       it('invalidates if last event is ::Invalidate', () => {
@@ -509,9 +556,10 @@ Object {
           {name: 'largestContentfulPaint::Candidate', cat, args, ts: 1000, duration: 10},
           {name: 'largestContentfulPaint::Invalidate', cat, args, ts: 1100, duration: 10}
         );
-        const trace = TraceProcessor.computeTraceOfTab(testTrace);
-        assert.equal(trace.largestContentfulPaintEvt, undefined);
-        assert.ok(trace.lcpInvalidated);
+        const trace = TraceProcessor.processTrace(testTrace);
+        const navigation = TraceProcessor.processNavigation(trace);
+        assert.equal(navigation.largestContentfulPaintEvt, undefined);
+        assert.ok(navigation.lcpInvalidated);
       });
 
       it('ignores candidates before timeOrigin', () => {
@@ -522,31 +570,33 @@ Object {
         testTrace.traceEvents.push(
           {name: 'largestContentfulPaint::Candidate', cat, args, ts: 1000, duration: 10}
         );
-        const trace = TraceProcessor.computeTraceOfTab(testTrace);
-        assert.equal(trace.largestContentfulPaintEvt, undefined);
-        assert.ok(!trace.lcpInvalidated);
+        const trace = TraceProcessor.processTrace(testTrace);
+        const navigation = TraceProcessor.processNavigation(trace);
+        assert.equal(navigation.largestContentfulPaintEvt, undefined);
+        assert.ok(!navigation.lcpInvalidated);
       });
     });
 
-    describe('finds correct metrics from all frames', () => {
+    describe('.processNavigation() - All Frames', () => {
       it('in a trace', () => {
-        const trace = TraceProcessor.computeTraceOfTab(lcpAllFramesTrace);
+        const trace = TraceProcessor.processTrace(lcpAllFramesTrace);
+        const navigation = TraceProcessor.processNavigation(trace);
         expect({
           // Main frame
           'mainFrameIds.frameId': trace.mainFrameIds.frameId,
-          'firstContentfulPaintEvt.ts': trace.firstContentfulPaintEvt.ts,
-          'largestContentfulPaintEvt.ts': trace.largestContentfulPaintEvt.ts,
-          'timestamps.firstContentfulPaint': trace.timestamps.firstContentfulPaint,
-          'timestamps.largestContentfulPaint': trace.timestamps.largestContentfulPaint,
-          'timings.firstContentfulPaint': trace.timings.firstContentfulPaint,
-          'timings.largestContentfulPaint': trace.timings.largestContentfulPaint,
+          'firstContentfulPaintEvt.ts': navigation.firstContentfulPaintEvt.ts,
+          'largestContentfulPaintEvt.ts': navigation.largestContentfulPaintEvt.ts,
+          'timestamps.firstContentfulPaint': navigation.timestamps.firstContentfulPaint,
+          'timestamps.largestContentfulPaint': navigation.timestamps.largestContentfulPaint,
+          'timings.firstContentfulPaint': navigation.timings.firstContentfulPaint,
+          'timings.largestContentfulPaint': navigation.timings.largestContentfulPaint,
           // All frames
-          'firstContentfulPaintAllFramesEvt.ts': trace.firstContentfulPaintAllFramesEvt.ts,
-          'largestContentfulPaintAllFramesEvt.ts': trace.largestContentfulPaintAllFramesEvt.ts,
-          'timestamps.firstContentfulPaintAllFrames': trace.timestamps.firstContentfulPaintAllFrames, // eslint-disable-line max-len
-          'timestamps.largestContentfulPaintAllFrames': trace.timestamps.largestContentfulPaintAllFrames, // eslint-disable-line max-len
-          'timings.firstContentfulPaintAllFrames': trace.timings.firstContentfulPaintAllFrames,
-          'timings.largestContentfulPaintAllFrames': trace.timings.largestContentfulPaintAllFrames,
+          'firstContentfulPaintAllFramesEvt.ts': navigation.firstContentfulPaintAllFramesEvt.ts,
+          'largestContentfulPaintAllFramesEvt.ts': navigation.largestContentfulPaintAllFramesEvt.ts,
+          'timestamps.firstContentfulPaintAllFrames': navigation.timestamps.firstContentfulPaintAllFrames, // eslint-disable-line max-len
+          'timestamps.largestContentfulPaintAllFrames': navigation.timestamps.largestContentfulPaintAllFrames, // eslint-disable-line max-len
+          'timings.firstContentfulPaintAllFrames': navigation.timings.firstContentfulPaintAllFrames,
+          'timings.largestContentfulPaintAllFrames': navigation.timings.largestContentfulPaintAllFrames, // eslint-disable-line max-len
         }).toMatchInlineSnapshot(`
           Object {
             "firstContentfulPaintAllFramesEvt.ts": 23466705983,
@@ -584,9 +634,10 @@ Object {
           {name: 'firstContentfulPaint', cat, args: {frame: mainFrame}, ts: 1100, duration: 10}
           /* eslint-enable max-len */
         );
-        const trace = TraceProcessor.computeTraceOfTab(testTrace);
-        assert.equal(trace.timestamps.firstContentfulPaint, 1100);
-        assert.equal(trace.timestamps.firstContentfulPaintAllFrames, 1000);
+        const trace = TraceProcessor.processTrace(testTrace);
+        const navigation = TraceProcessor.processNavigation(trace);
+        assert.equal(navigation.timestamps.firstContentfulPaint, 1100);
+        assert.equal(navigation.timestamps.firstContentfulPaintAllFrames, 1000);
       });
 
       it('finds LCP from all frames', () => {
@@ -606,15 +657,17 @@ Object {
           {name: 'largestContentfulPaint::Candidate', cat, args: {data: {size: 100}, frame: mainFrame}, ts: 1500, duration: 10}
           /* eslint-enable max-len */
         );
-        const trace = TraceProcessor.computeTraceOfTab(testTrace);
-        assert.equal(trace.timestamps.largestContentfulPaint, 1500);
-        assert.equal(trace.timestamps.largestContentfulPaintAllFrames, 1400);
-        assert.ok(!trace.lcpInvalidated);
+        const trace = TraceProcessor.processTrace(testTrace);
+        const navigation = TraceProcessor.processNavigation(trace);
+        assert.equal(navigation.timestamps.largestContentfulPaint, 1500);
+        assert.equal(navigation.timestamps.largestContentfulPaintAllFrames, 1400);
+        assert.ok(!navigation.lcpInvalidated);
       });
     });
 
     it('handles traces missing a paints (captured in background tab)', () => {
-      const trace = TraceProcessor.computeTraceOfTab(backgroundTabTrace);
+      const trace = TraceProcessor.processTrace(backgroundTabTrace);
+      const navigation = TraceProcessor.processNavigation(trace);
       assert.equal(trace.mainFrameIds.frameId, '0x53965941e30');
       assert.notEqual(trace.timeOriginEvt.ts, 1966813346529, 'picked wrong frame');
       assert.notEqual(trace.timeOriginEvt.ts, 1966813520313, 'picked wrong frame');
@@ -623,7 +676,7 @@ Object {
         1966813258737,
         'didnt select navStart event with same timestamp as usertiming measure'
       );
-      assert.equal(trace.firstMeaningfulPaintEvt, undefined, 'bad fmp');
+      assert.equal(navigation.firstMeaningfulPaintEvt, undefined, 'bad fmp');
     });
 
     it('handles traces with TracingStartedInBrowser events', () => {
@@ -681,29 +734,16 @@ Object {
           'name': 'thread_name',
           'args': {'name': 'CrRendererMain'},
         }]};
-      const trace = TraceProcessor.computeTraceOfTab(tracingStartedInBrowserTrace);
+      const trace = TraceProcessor.processTrace(tracingStartedInBrowserTrace);
       assert.equal(trace.mainFrameIds.frameId, 'B192D1F3355A6F961EC8F0B01623C1FB');
       assert.equal(trace.timeOriginEvt.ts, 2193564790059);
     });
 
     it('handles no TracingStarted errors in m74+', () => {
-      const trace = TraceProcessor.computeTraceOfTab(noTracingStartedTrace);
+      const trace = TraceProcessor.processTrace(noTracingStartedTrace);
+      const navigation = TraceProcessor.processNavigation(trace);
       expect(trace.mainFrameIds.frameId).toEqual('0E0B1AF0B1BA04676037345D18A71577');
-      expect(trace.firstContentfulPaintEvt.ts).toEqual(2610265036367);
-    });
-
-    it('sorts events by increasing timestamp', () => {
-      const trace = JSON.parse(fs.readFileSync(__dirname +
-          '/../../fixtures/traces/tracingstarted-after-navstart.json', 'utf8'));
-      const shuffledEvents = trace.traceEvents.slice().sort(() => Math.random() * 2 - 1);
-      const traceOfTab = TraceProcessor.computeTraceOfTab({traceEvents: shuffledEvents});
-
-      let lastTs = -Infinity;
-      for (const event of traceOfTab.processEvents) {
-        if (!event.ts) continue;
-        expect(event.ts).toBeGreaterThanOrEqual(lastTs);
-        lastTs = event.ts;
-      }
+      expect(navigation.firstContentfulPaintEvt.ts).toEqual(2610265036367);
     });
 
     describe('#filteredTraceSort', () => {
@@ -810,9 +850,8 @@ Object {
       });
     });
 
-
     it('throws on traces missing a navigationStart', () => {
-      expect(() => TraceProcessor.computeTraceOfTab(noNavStartTrace))
+      expect(() => TraceProcessor.processTrace(noNavStartTrace))
         .toThrowError('navigationStart');
     });
 
@@ -821,14 +860,14 @@ Object {
         traceEvents: pwaTrace.filter(e => e.name !== 'ResourceSendRequest'),
       };
 
-      expect(() => TraceProcessor.computeTraceOfTab(traceWithoutResourceSend, {
+      expect(() => TraceProcessor.processTrace(traceWithoutResourceSend, {
         timeOriginDeterminationMethod: 'firstResourceSendRequest',
       }))
         .toThrowError('ResourceSendRequest');
     });
 
     it('does not throw on traces missing an FCP', () => {
-      expect(() => TraceProcessor.computeTraceOfTab(noFCPtrace)).not.toThrow();
+      expect(() => TraceProcessor.processTrace(noFCPtrace)).not.toThrow();
     });
   });
 });
diff --git a/lighthouse-core/test/results/artifacts/artifacts.json b/lighthouse-core/test/results/artifacts/artifacts.json
index 8fc3e11b7..cba3a2af3 100644
--- a/lighthouse-core/test/results/artifacts/artifacts.json
+++ b/lighthouse-core/test/results/artifacts/artifacts.json
@@ -145,6 +145,7 @@
     "onlyCategories": null,
     "skipAudits": null
   },
+  "GatherContext": {"gatherMode": "navigation"},
   "Timing": [],
   "LinkElements": [
     {
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index 74a44e885..3fbafe5ec 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -113,56 +113,56 @@
       "scoreDisplayMode": "informative",
       "details": {
         "type": "filmstrip",
-        "scale": 4927.278,
+        "scale": 4791,
         "items": [
           {
-            "timing": 493,
-            "timestamp": 185603812639.80002,
+            "timing": 479,
+            "timestamp": 185603799012,
             "data": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRQBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIANUAeAMBEQACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AP1ToAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgD//Z"
           },
           {
-            "timing": 985,
-            "timestamp": 185604305367.6,
+            "timing": 958,
+            "timestamp": 185604278112,
             "data": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRQBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIANUAeAMBEQACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AP1ToAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgD//Z"
           },
           {
-            "timing": 1478,
-            "timestamp": 185604798095.4,
+            "timing": 1437,
+            "timestamp": 185604757212,
             "data": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRQBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIANUAeAMBEQACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AP1ToAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgD//Z"
           },
           {
-            "timing": 1971,
-            "timestamp": 185605290823.19998,
+            "timing": 1916,
+            "timestamp": 185605236312,
             "data": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRQBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIANUAeAMBEQACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AP1ToAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgD//Z"
           },
           {
-            "timing": 2464,
-            "timestamp": 185605783551,
+            "timing": 2396,
+            "timestamp": 185605715412,
             "data": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRQBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIANUAeAMBEQACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AP1ToAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgD//Z"
           },
           {
-            "timing": 2956,
-            "timestamp": 185606276278.80002,
+            "timing": 2875,
+            "timestamp": 185606194512,
             "data": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRQBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIANUAeAMBEQACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AP1ToAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgD//Z"
           },
           {
-            "timing": 3449,
-            "timestamp": 185606769006.6,
+            "timing": 3354,
+            "timestamp": 185606673612,
             "data": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRQBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIANUAeAMBEQACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AP1ToAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgD//Z"
           },
           {
-            "timing": 3942,
-            "timestamp": 185607261734.4,
+            "timing": 3833,
+            "timestamp": 185607152712,
             "data": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRQBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIANUAeAMBEQACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AP1ToAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgD//Z"
           },
           {
-            "timing": 4435,
-            "timestamp": 185607754462.19998,
+            "timing": 4312,
+            "timestamp": 185607631812,
             "data": "data:image/jpeg;base64,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BcA634e/BXxh8PvH95cW2neCm8I6rFpcr2kQmSTRJbRDGYbOIxFJE2BQjl4yrMzFDnZQBy2jfs1ePdG8N6Bp4n8OvPpGh+KNNjLXc5jlfUbhZrVyBbj5RtAkGeAfl30AdX47+Bvijxl8XtE8YIuhWUNrqeg3lygupWldbFdSMuP3OC2+/UKDjKxkkqTtoA7/AOAHgTUvhf8ABXwb4S1h7STU9G06OymewkaSBig27kJVTg46FQR0oA7+gAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA//9k="
           },
           {
-            "timing": 4927,
-            "timestamp": 185608247190,
+            "timing": 4791,
+            "timestamp": 185608110912,
             "data": "data:image/jpeg;base64,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"
           }
         ]
@@ -5960,7 +5960,13 @@
       },
       {
         "startTime": 0,
-        "name": "lh:computed:TraceOfTab",
+        "name": "lh:computed:ProcessedTrace",
+        "duration": 100,
+        "entryType": "measure"
+      },
+      {
+        "startTime": 0,
+        "name": "lh:computed:ProcessedNavigation",
         "duration": 100,
         "entryType": "measure"
       },
@@ -6012,12 +6018,6 @@
         "duration": 100,
         "entryType": "measure"
       },
-      {
-        "startTime": 0,
-        "name": "lh:computed:Interactive",
-        "duration": 100,
-        "entryType": "measure"
-      },
       {
         "startTime": 0,
         "name": "lh:audit:final-screenshot",
@@ -6264,12 +6264,6 @@
         "duration": 100,
         "entryType": "measure"
       },
-      {
-        "startTime": 0,
-        "name": "lh:computed:LoadSimulator",
-        "duration": 100,
-        "entryType": "measure"
-      },
       {
         "startTime": 0,
         "name": "lh:audit:uses-rel-preconnect",
@@ -6426,12 +6420,6 @@
         "duration": 100,
         "entryType": "measure"
       },
-      {
-        "startTime": 0,
-        "name": "lh:computed:LanternFirstContentfulPaint",
-        "duration": 100,
-        "entryType": "measure"
-      },
       {
         "startTime": 0,
         "name": "lh:audit:csp-xss",
@@ -6846,6 +6834,12 @@
         "duration": 100,
         "entryType": "measure"
       },
+      {
+        "startTime": 0,
+        "name": "lh:computed:LoadSimulator",
+        "duration": 100,
+        "entryType": "measure"
+      },
       {
         "startTime": 0,
         "name": "lh:audit:unminified-css",
diff --git a/third-party/chromium-webtests/webtests/http/tests/devtools/lighthouse/lighthouse-successful-run-expected.txt b/third-party/chromium-webtests/webtests/http/tests/devtools/lighthouse/lighthouse-successful-run-expected.txt
index 722faaf3e..1bbaa0320 100644
--- a/third-party/chromium-webtests/webtests/http/tests/devtools/lighthouse/lighthouse-successful-run-expected.txt
+++ b/third-party/chromium-webtests/webtests/http/tests/devtools/lighthouse/lighthouse-successful-run-expected.txt
@@ -39,6 +39,7 @@ Gathering setup: MetaElements
 Gathering setup: ScriptElements
 Gathering setup: IFrameElements
 Gathering setup: MainDocumentContent
+Gathering setup: GatherContext
 Gathering setup: GlobalListeners
 Gathering setup: AppCacheManifest
 Gathering setup: Doctype
@@ -70,6 +71,7 @@ Gathering in-page: MetaElements
 Gathering in-page: ScriptElements
 Gathering in-page: IFrameElements
 Gathering in-page: MainDocumentContent
+Gathering in-page: GatherContext
 Gathering in-page: GlobalListeners
 Gathering in-page: AppCacheManifest
 Gathering in-page: Doctype
@@ -102,6 +104,7 @@ Gathering: MetaElements
 Gathering: ScriptElements
 Gathering: IFrameElements
 Gathering: MainDocumentContent
+Gathering: GatherContext
 Gathering: GlobalListeners
 Gathering: AppCacheManifest
 Gathering: Doctype
@@ -117,6 +120,8 @@ Getting browser version
 Gathering: TapTargets
 Gathering: Accessibility
 Gathering: TraceElements
+Computing artifact: ProcessedTrace
+Computing artifact: ProcessedNavigation
 Gathering: InspectorIssues
 Gathering: SourceMaps
 Gathering: FullPageScreenshot
@@ -158,7 +163,6 @@ Auditing: Has a `<meta name="viewport">` tag with `width` or `initial-scale`
 Computing artifact: ViewportMeta
 Auditing: First Contentful Paint
 Computing artifact: FirstContentfulPaint
-Computing artifact: TraceOfTab
 Computing artifact: LanternFirstContentfulPaint
 Computing artifact: PageDependencyGraph
 Computing artifact: LoadSimulator
@@ -212,7 +216,6 @@ Auditing: Minimizes main-thread work
 Computing artifact: MainThreadTasks
 Auditing: JavaScript execution time
 Auditing: Preload key requests
-Computing artifact: LoadSimulator
 Auditing: Preconnect to required origins
 Auditing: All text remains visible during webfont loads
 Auditing: Diagnostics
@@ -236,8 +239,6 @@ Auditing: Avoid non-composited animations
 Auditing: Image elements have explicit `width` and `height`
 Auditing: Page has valid source maps
 Auditing: Preload Largest Contentful Paint image
-Computing artifact: LanternLargestContentfulPaint
-Computing artifact: LanternFirstContentfulPaint
 Auditing: Ensure CSP is effective against XSS attacks
 Auditing: Full-page screenshot
 Auditing: Script Treemap Data
@@ -306,7 +307,6 @@ Auditing: Defer offscreen images
 Auditing: Eliminate render-blocking resources
 Computing artifact: UnusedCSS
 Computing artifact: FirstContentfulPaint
-Computing artifact: LanternFirstContentfulPaint
 Auditing: Minify CSS
 Auditing: Minify JavaScript
 Auditing: Reduce unused CSS
@@ -361,6 +361,7 @@ Auditing: No ad found at the very top of the viewport
 Auditing: Few or no ads loaded outside viewport
 Auditing: Ad tag is loaded asynchronously
 Auditing: Ads not blocked by load events
+Computing artifact: TraceOfTab
 Auditing: No bottleneck requests found
 Auditing: No duplicate tags found
 Auditing: Latency of first ad render