Repository: GoogleChrome/lighthouse (js)

Issue:
Make desktop perf metric score curves identical everywhere
When running tests on Lighthouse and PageSpeed Insight, we spotted some inconsistencies in the `speed index` classification of what is a 'fast', i.e. green/orange/red: 

- In Lighthouse, a `speed index` of 3.2s is green

![lighthouse](https://user-images.githubusercontent.com/2845540/61710701-7ac27a80-ad52-11e9-89c6-5618c6ab4c4b.png)


- In Page Speed Insights, a `speed index` of 1.6s is orange

![pagespeed_insights](https://user-images.githubusercontent.com/2845540/61710692-75653000-ad52-11e9-9113-ecc8bc550f8d.png)

Both results are observed in Desktop mode.

Since `speed index` is a Lab Data field, I expected the 2 tools to give the same classification.

Pull request description:
core: use the same scoring curve for desktop in all channels
Fixes #9436.

The first inclination was to add some overrides in `config.js`, [kinda like this](https://github.com/GoogleChrome/lighthouse/blob/7637f0f69d978e3dcc8e88b744414d9c69065f71/lighthouse-core/config/config.js#L534-L549). But having the scoring curve numbers co-located with the audits is just so good, so let's try to keep them together.

The next inclination was to modify the audit `context` to have `hostDevice`. However, this was kind of awkward. For example, there's no great place to keep the logic of host agent checking, which is needed in creating the base artifacts and now here.

So instead of avoiding making a new artifact, I suggest we embrace it. This will be useful for #9713 too.

~~Draft to get consensus. I only did one audit.~~

Patch:
diff --git a/lighthouse-core/audits/metrics/first-contentful-paint.js b/lighthouse-core/audits/metrics/first-contentful-paint.js
index 7cf769571..75653041e 100644
--- a/lighthouse-core/audits/metrics/first-contentful-paint.js
+++ b/lighthouse-core/audits/metrics/first-contentful-paint.js
@@ -27,20 +27,27 @@ class FirstContentfulPaint extends Audit {
       title: str_(i18n.UIStrings.firstContentfulPaintMetric),
       description: str_(UIStrings.description),
       scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
-      requiredArtifacts: ['traces', 'devtoolsLogs'],
+      requiredArtifacts: ['traces', 'devtoolsLogs', 'TestedAsMobileDevice'],
     };
   }
 
   /**
-   * @return {LH.Audit.ScoreOptions}
+   * @return {{mobile: LH.Audit.ScoreOptions, desktop: LH.Audit.ScoreOptions}}
    */
   static get defaultOptions() {
     return {
-      // 75th and 95th percentiles HTTPArchive -> median and PODR
-      // https://bigquery.cloud.google.com/table/httparchive:lighthouse.2018_04_01_mobile?pli=1
-      // see https://www.desmos.com/calculator/2t1ugwykrl
-      scorePODR: 2000,
-      scoreMedian: 4000,
+      mobile: {
+        // 75th and 95th percentiles HTTPArchive -> median and PODR
+        // https://bigquery.cloud.google.com/table/httparchive:lighthouse.2018_04_01_mobile?pli=1
+        // see https://www.desmos.com/calculator/2t1ugwykrl
+        scorePODR: 2000,
+        scoreMedian: 4000,
+      },
+      desktop: {
+        // SELECT QUANTILES(renderStart, 21) FROM [httparchive:summary_pages.2018_12_15_desktop] LIMIT 1000
+        scorePODR: 800,
+        scoreMedian: 1600,
+      },
     };
   }
 
@@ -54,12 +61,14 @@ class FirstContentfulPaint extends Audit {
     const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
     const metricComputationData = {trace, devtoolsLog, settings: context.settings};
     const metricResult = await ComputedFcp.request(metricComputationData, context);
+    const scoreOptions =
+      context.options[artifacts.TestedAsMobileDevice === false ? 'desktop' : 'mobile'];
 
     return {
       score: Audit.computeLogNormalScore(
         metricResult.timing,
-        context.options.scorePODR,
-        context.options.scoreMedian
+        scoreOptions.scorePODR,
+        scoreOptions.scoreMedian
       ),
       numericValue: metricResult.timing,
       displayValue: str_(i18n.UIStrings.seconds, {timeInMs: metricResult.timing}),
diff --git a/lighthouse-core/audits/metrics/first-cpu-idle.js b/lighthouse-core/audits/metrics/first-cpu-idle.js
index bab590259..d2a2c9582 100644
--- a/lighthouse-core/audits/metrics/first-cpu-idle.js
+++ b/lighthouse-core/audits/metrics/first-cpu-idle.js
@@ -27,20 +27,27 @@ class FirstCPUIdle extends Audit {
       title: str_(i18n.UIStrings.firstCPUIdleMetric),
       description: str_(UIStrings.description),
       scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
-      requiredArtifacts: ['traces', 'devtoolsLogs'],
+      requiredArtifacts: ['traces', 'devtoolsLogs', 'TestedAsMobileDevice'],
     };
   }
 
   /**
-   * @return {LH.Audit.ScoreOptions}
+   * @return {{mobile: LH.Audit.ScoreOptions, desktop: LH.Audit.ScoreOptions}}
    */
   static get defaultOptions() {
     return {
-      // 75th and 95th percentiles HTTPArchive -> median and PODR
-      // https://bigquery.cloud.google.com/table/httparchive:lighthouse.2018_04_01_mobile?pli=1
-      // see https://www.desmos.com/calculator/yv89gz2nwf
-      scorePODR: 2900,
-      scoreMedian: 6500,
+      mobile: {
+        // 75th and 95th percentiles HTTPArchive -> median and PODR
+        // https://bigquery.cloud.google.com/table/httparchive:lighthouse.2018_04_01_mobile?pli=1
+        // see https://www.desmos.com/calculator/yv89gz2nwf
+        scorePODR: 2900,
+        scoreMedian: 6500,
+      },
+      desktop: {
+        // SELECT QUANTILES(fullyLoaded, 21) FROM [httparchive:summary_pages.2018_12_15_desktop] LIMIT 1000
+        scorePODR: 2000,
+        scoreMedian: 4500,
+      },
     };
   }
 
@@ -57,12 +64,14 @@ class FirstCPUIdle extends Audit {
     const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
     const metricComputationData = {trace, devtoolsLog, settings: context.settings};
     const metricResult = await ComputedFci.request(metricComputationData, context);
+    const scoreOptions =
+      context.options[artifacts.TestedAsMobileDevice === false ? 'desktop' : 'mobile'];
 
     return {
       score: Audit.computeLogNormalScore(
         metricResult.timing,
-        context.options.scorePODR,
-        context.options.scoreMedian
+        scoreOptions.scorePODR,
+        scoreOptions.scoreMedian
       ),
       numericValue: metricResult.timing,
       displayValue: str_(i18n.UIStrings.seconds, {timeInMs: metricResult.timing}),
diff --git a/lighthouse-core/audits/metrics/first-meaningful-paint.js b/lighthouse-core/audits/metrics/first-meaningful-paint.js
index 2189e721b..0bfacc2a1 100644
--- a/lighthouse-core/audits/metrics/first-meaningful-paint.js
+++ b/lighthouse-core/audits/metrics/first-meaningful-paint.js
@@ -27,20 +27,27 @@ class FirstMeaningfulPaint extends Audit {
       title: str_(i18n.UIStrings.firstMeaningfulPaintMetric),
       description: str_(UIStrings.description),
       scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
-      requiredArtifacts: ['traces', 'devtoolsLogs'],
+      requiredArtifacts: ['traces', 'devtoolsLogs', 'TestedAsMobileDevice'],
     };
   }
 
   /**
-   * @return {LH.Audit.ScoreOptions}
+   * @return {{mobile: LH.Audit.ScoreOptions, desktop: LH.Audit.ScoreOptions}}
    */
   static get defaultOptions() {
     return {
-      // 75th and 95th percentiles HTTPArchive -> median and PODR
-      // https://bigquery.cloud.google.com/table/httparchive:lighthouse.2018_04_01_mobile?pli=1
-      // see https://www.desmos.com/calculator/2t1ugwykrl
-      scorePODR: 2000,
-      scoreMedian: 4000,
+      mobile: {
+        // 75th and 95th percentiles HTTPArchive -> median and PODR
+        // https://bigquery.cloud.google.com/table/httparchive:lighthouse.2018_04_01_mobile?pli=1
+        // see https://www.desmos.com/calculator/2t1ugwykrl
+        scorePODR: 2000,
+        scoreMedian: 4000,
+      },
+      desktop: {
+        // SELECT QUANTILES(renderStart, 21) FROM [httparchive:summary_pages.2018_12_15_desktop] LIMIT 1000
+        scorePODR: 800,
+        scoreMedian: 1600,
+      },
     };
   }
 
@@ -57,12 +64,14 @@ class FirstMeaningfulPaint extends Audit {
     const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
     const metricComputationData = {trace, devtoolsLog, settings: context.settings};
     const metricResult = await ComputedFmp.request(metricComputationData, context);
+    const scoreOptions =
+      context.options[artifacts.TestedAsMobileDevice === false ? 'desktop' : 'mobile'];
 
     return {
       score: Audit.computeLogNormalScore(
         metricResult.timing,
-        context.options.scorePODR,
-        context.options.scoreMedian
+        scoreOptions.scorePODR,
+        scoreOptions.scoreMedian
       ),
       numericValue: metricResult.timing,
       displayValue: str_(i18n.UIStrings.seconds, {timeInMs: metricResult.timing}),
diff --git a/lighthouse-core/audits/metrics/interactive.js b/lighthouse-core/audits/metrics/interactive.js
index 41358fd0d..c4a162257 100644
--- a/lighthouse-core/audits/metrics/interactive.js
+++ b/lighthouse-core/audits/metrics/interactive.js
@@ -33,20 +33,27 @@ class InteractiveMetric extends Audit {
       title: str_(i18n.UIStrings.interactiveMetric),
       description: str_(UIStrings.description),
       scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
-      requiredArtifacts: ['traces', 'devtoolsLogs'],
+      requiredArtifacts: ['traces', 'devtoolsLogs', 'TestedAsMobileDevice'],
     };
   }
 
   /**
-   * @return {LH.Audit.ScoreOptions}
+   * @return {{mobile: LH.Audit.ScoreOptions, desktop: LH.Audit.ScoreOptions}}
    */
   static get defaultOptions() {
     return {
-      // 75th and 95th percentiles HTTPArchive -> median and PODR
-      // https://bigquery.cloud.google.com/table/httparchive:lighthouse.2018_04_01_mobile?pli=1
-      // see https://www.desmos.com/calculator/5xgy0pyrbp
-      scorePODR: 2900,
-      scoreMedian: 7300,
+      mobile: {
+        // 75th and 95th percentiles HTTPArchive -> median and PODR
+        // https://bigquery.cloud.google.com/table/httparchive:lighthouse.2018_04_01_mobile?pli=1
+        // see https://www.desmos.com/calculator/5xgy0pyrbp
+        scorePODR: 2900,
+        scoreMedian: 7300,
+      },
+      desktop: {
+        // SELECT QUANTILES(fullyLoaded, 21) FROM [httparchive:summary_pages.2018_12_15_desktop] LIMIT 1000
+        scorePODR: 2000,
+        scoreMedian: 4500,
+      },
     };
   }
 
@@ -61,6 +68,8 @@ class InteractiveMetric extends Audit {
     const metricComputationData = {trace, devtoolsLog, settings: context.settings};
     const metricResult = await Interactive.request(metricComputationData, context);
     const timeInMs = metricResult.timing;
+    const scoreOptions =
+      context.options[artifacts.TestedAsMobileDevice === false ? 'desktop' : 'mobile'];
     const extendedInfo = {
       timeInMs,
       timestamp: metricResult.timestamp,
@@ -73,8 +82,8 @@ class InteractiveMetric extends Audit {
     return {
       score: Audit.computeLogNormalScore(
         timeInMs,
-        context.options.scorePODR,
-        context.options.scoreMedian
+        scoreOptions.scorePODR,
+        scoreOptions.scoreMedian
       ),
       numericValue: timeInMs,
       displayValue: str_(i18n.UIStrings.seconds, {timeInMs}),
diff --git a/lighthouse-core/audits/metrics/speed-index.js b/lighthouse-core/audits/metrics/speed-index.js
index 3f6801def..9780c069e 100644
--- a/lighthouse-core/audits/metrics/speed-index.js
+++ b/lighthouse-core/audits/metrics/speed-index.js
@@ -27,20 +27,27 @@ class SpeedIndex extends Audit {
       title: str_(i18n.UIStrings.speedIndexMetric),
       description: str_(UIStrings.description),
       scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
-      requiredArtifacts: ['traces', 'devtoolsLogs'],
+      requiredArtifacts: ['traces', 'devtoolsLogs', 'TestedAsMobileDevice'],
     };
   }
 
   /**
-   * @return {LH.Audit.ScoreOptions}
+   * @return {{mobile: LH.Audit.ScoreOptions, desktop: LH.Audit.ScoreOptions}}
    */
   static get defaultOptions() {
     return {
-      // 75th and 95th percentiles HTTPArchive -> median and PODR
-      // https://bigquery.cloud.google.com/table/httparchive:lighthouse.2018_04_01_mobile?pli=1
-      // see https://www.desmos.com/calculator/orvoyu9ygq
-      scorePODR: 2900,
-      scoreMedian: 5800,
+      mobile: {
+        // 75th and 95th percentiles HTTPArchive -> median and PODR
+        // https://bigquery.cloud.google.com/table/httparchive:lighthouse.2018_04_01_mobile?pli=1
+        // see https://www.desmos.com/calculator/orvoyu9ygq
+        scorePODR: 2900,
+        scoreMedian: 5800,
+      },
+      desktop: {
+        // SELECT QUANTILES(SpeedIndex, 21) FROM [httparchive:summary_pages.2018_12_15_desktop] LIMIT 1000
+        scorePODR: 1100,
+        scoreMedian: 2300,
+      },
     };
   }
 
@@ -56,12 +63,14 @@ class SpeedIndex extends Audit {
     const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
     const metricComputationData = {trace, devtoolsLog, settings: context.settings};
     const metricResult = await ComputedSi.request(metricComputationData, context);
+    const scoreOptions =
+      context.options[artifacts.TestedAsMobileDevice === false ? 'desktop' : 'mobile'];
 
     return {
       score: Audit.computeLogNormalScore(
         metricResult.timing,
-        context.options.scorePODR,
-        context.options.scoreMedian
+        scoreOptions.scorePODR,
+        scoreOptions.scoreMedian
       ),
       numericValue: metricResult.timing,
       displayValue: str_(i18n.UIStrings.seconds, {timeInMs: metricResult.timing}),
diff --git a/lighthouse-core/config/config.js b/lighthouse-core/config/config.js
index da29854e5..c90743fee 100644
--- a/lighthouse-core/config/config.js
+++ b/lighthouse-core/config/config.js
@@ -29,6 +29,7 @@ const BASE_ARTIFACT_BLANKS = {
   fetchTime: '',
   LighthouseRunWarnings: '',
   TestedAsMobileDevice: '',
+  HostFormFactor: '',
   HostUserAgent: '',
   NetworkUserAgent: '',
   BenchmarkIndex: '',
diff --git a/lighthouse-core/config/lr-desktop-config.js b/lighthouse-core/config/lr-desktop-config.js
index e4de4934c..25f959daf 100644
--- a/lighthouse-core/config/lr-desktop-config.js
+++ b/lighthouse-core/config/lr-desktop-config.js
@@ -22,19 +22,6 @@ const config = {
     // Skip the h2 audit so it doesn't lie to us. See https://github.com/GoogleChrome/lighthouse/issues/6539
     skipAudits: ['uses-http2'],
   },
-  audits: [
-    // 75th and 95th percentiles -> median and PODR
-    // SELECT QUANTILES(renderStart, 21) FROM [httparchive:summary_pages.2018_12_15_desktop] LIMIT 1000
-    {path: 'metrics/first-contentful-paint', options: {scorePODR: 800, scoreMedian: 1600}},
-    {path: 'metrics/first-meaningful-paint', options: {scorePODR: 800, scoreMedian: 1600}},
-    // 75th and 95th percentiles -> median and PODR
-    // SELECT QUANTILES(SpeedIndex, 21) FROM [httparchive:summary_pages.2018_12_15_desktop] LIMIT 1000
-    {path: 'metrics/speed-index', options: {scorePODR: 1100, scoreMedian: 2300}},
-    // 75th and 95th percentiles -> median and PODR
-    // SELECT QUANTILES(fullyLoaded, 21) FROM [httparchive:summary_pages.2018_12_15_desktop] LIMIT 1000
-    {path: 'metrics/interactive', options: {scorePODR: 2000, scoreMedian: 4500}},
-    {path: 'metrics/first-cpu-idle', options: {scorePODR: 2000, scoreMedian: 4500}},
-  ],
 };
 
 module.exports = config;
diff --git a/lighthouse-core/gather/gather-runner.js b/lighthouse-core/gather/gather-runner.js
index 03360baeb..8e094289a 100644
--- a/lighthouse-core/gather/gather-runner.js
+++ b/lighthouse-core/gather/gather-runner.js
@@ -456,14 +456,16 @@ class GatherRunner {
 
     const {emulatedFormFactor} = options.settings;
     // Whether Lighthouse was run on a mobile device (i.e. not on a desktop machine).
-    const IsMobileHost = hostUserAgent.includes('Android') || hostUserAgent.includes('Mobile');
+    const HostFormFactor = hostUserAgent.includes('Android') || hostUserAgent.includes('Mobile') ?
+      'mobile' : 'desktop';
     const TestedAsMobileDevice = emulatedFormFactor === 'mobile' ||
-      (emulatedFormFactor !== 'desktop' && IsMobileHost);
+      (emulatedFormFactor !== 'desktop' && HostFormFactor === 'mobile');
 
     return {
       fetchTime: (new Date()).toJSON(),
       LighthouseRunWarnings: [],
       TestedAsMobileDevice,
+      HostFormFactor,
       HostUserAgent: hostUserAgent,
       NetworkUserAgent: '', // updated later
       BenchmarkIndex: 0, // updated later
diff --git a/lighthouse-core/gather/gatherers/seo/font-size.js b/lighthouse-core/gather/gatherers/seo/font-size.js
index d84cdfa73..3e54da271 100644
--- a/lighthouse-core/gather/gatherers/seo/font-size.js
+++ b/lighthouse-core/gather/gatherers/seo/font-size.js
@@ -294,7 +294,14 @@ class FontSize extends Gatherer {
       .sort((a, b) => b.textLength - a.textLength)
       .slice(0, MAX_NODES_SOURCE_RULE_FETCHED)
       .map(async failingNode => {
-        failingNode.cssRule = await fetchSourceRule(driver, failingNode.node);
+        try {
+          const cssRule = await fetchSourceRule(driver, failingNode.node);
+          failingNode.cssRule = cssRule;
+        } catch (err) {
+          // The node was deleted. We don't need to distinguish between lack-of-rule
+          // due to a deleted node vs due to failed attribution, so just set to undefined.
+          failingNode.cssRule = undefined;
+        }
         return failingNode;
       });
 
@@ -338,6 +345,8 @@ class FontSize extends Gatherer {
 
     passContext.driver.off('CSS.styleSheetAdded', onStylesheetAdd);
 
+    // For the nodes whose computed style we could attribute to a stylesheet, assign
+    // the stylsheet to the data.
     analyzedFailingNodesData
       .filter(data => data.cssRule && data.cssRule.styleSheetId)
       // @ts-ignore - guaranteed to exist from the filter immediately above
diff --git a/lighthouse-core/scripts/build-report-for-autodeployment.js b/lighthouse-core/scripts/build-report-for-autodeployment.js
index 19d65330e..4bfe04077 100644
--- a/lighthouse-core/scripts/build-report-for-autodeployment.js
+++ b/lighthouse-core/scripts/build-report-for-autodeployment.js
@@ -77,6 +77,7 @@ async function generateErrorLHR() {
       `Something went wrong with recording the trace over your page load. Please run Lighthouse again. (NO_FCP)`, // eslint-disable-line max-len
     ],
     TestedAsMobileDevice: true,
+    HostFormFactor: 'desktop',
     HostUserAgent: 'Mozilla/5.0 ErrorUserAgent Chrome/66',
     NetworkUserAgent: 'Mozilla/5.0 ErrorUserAgent Chrome/66',
     BenchmarkIndex: 1000,
diff --git a/lighthouse-core/scripts/dogfood-lhci.sh b/lighthouse-core/scripts/dogfood-lhci.sh
index b80b7f428..d5394d150 100755
--- a/lighthouse-core/scripts/dogfood-lhci.sh
+++ b/lighthouse-core/scripts/dogfood-lhci.sh
@@ -39,6 +39,6 @@ yarn now-build
 # Install LHCI
 npm install -g @lhci/cli@next
 # Collect our LHCI results.
-lhci collect --build-dir=./dist/now/english/
+lhci collect --staticDistDir=./dist/now/english/
 # Upload the results to our canary server.
 lhci upload --serverBaseUrl="$LHCI_CANARY_SERVER_URL" --token="$LHCI_CANARY_SERVER_TOKEN" --github-token="$BUNDLESIZE_GITHUB_TOKEN"
diff --git a/lighthouse-core/scripts/roll-to-devtools.sh b/lighthouse-core/scripts/roll-to-devtools.sh
index 2de7ae2d8..00d9bfc3e 100755
--- a/lighthouse-core/scripts/roll-to-devtools.sh
+++ b/lighthouse-core/scripts/roll-to-devtools.sh
@@ -19,11 +19,9 @@ check="\033[96m ✓\033[39m"
 if [[ -n "$1" ]]; then
   frontend_dir="$1"
 else
-  frontend_dir="$chromium_dir/third_party/blink/renderer/devtools/front_end"
+  frontend_dir="$chromium_dir/third_party/devtools-frontend/src/front_end"
 fi
 
-tests_dir="$frontend_dir/../../../web_tests/http/tests/devtools/audits"
-
 if [[ ! -d "$frontend_dir" || ! -a "$frontend_dir/Runtime.js" ]]; then
   echo -e "\033[31m✖ Error!\033[39m"
   echo "This script requires a devtools frontend folder. We didn't find one here:"
diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts
index 7985035a1..5153a2565 100644
--- a/types/artifacts.d.ts
+++ b/types/artifacts.d.ts
@@ -29,6 +29,8 @@ declare global {
       LighthouseRunWarnings: string[];
       /** Whether the page was loaded on either a real or emulated mobile device. */
       TestedAsMobileDevice: boolean;
+      /** Device which Chrome is running on. */
+      HostFormFactor: 'desktop'|'mobile';
       /** The user agent string of the version of Chrome used. */
       HostUserAgent: string;
       /** The user agent string that Lighthouse used to load the page. */


Test patch:
diff --git a/lighthouse-cli/test/smokehouse/test-definitions/dobetterweb/dbw-expectations.js b/lighthouse-cli/test/smokehouse/test-definitions/dobetterweb/dbw-expectations.js
index 33e2b37ea..2e332b4f7 100644
--- a/lighthouse-cli/test/smokehouse/test-definitions/dobetterweb/dbw-expectations.js
+++ b/lighthouse-cli/test/smokehouse/test-definitions/dobetterweb/dbw-expectations.js
@@ -12,6 +12,8 @@
 const expectations = [
   {
     artifacts: {
+      HostFormFactor: 'desktop',
+      TestedAsMobileDevice: true,
       Stacks: [{
         id: 'jquery',
       }, {
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 43dd9ea19..a2ddb5d21 100644
--- a/lighthouse-core/test/computed/metrics/first-contentful-paint-test.js
+++ b/lighthouse-core/test/computed/metrics/first-contentful-paint-test.js
@@ -30,7 +30,7 @@ describe('Metrics: FCP', () => {
     assert.ok(result.pessimisticGraph, 'should have created pessimistic graph');
   });
 
-  it('should compute an observed value', async () => {
+  it('should compute an observed value (desktop)', async () => {
     const settings = {throttlingMethod: 'provided'};
     const context = {settings, computedCache: new Map()};
     const result = await FirstContentfulPaint.request({trace, devtoolsLog, settings}, context);
@@ -38,4 +38,14 @@ describe('Metrics: FCP', () => {
     assert.equal(Math.round(result.timing), 499);
     assert.equal(result.timestamp, 225414670885);
   });
+
+  it('should compute an observed value (mobile)', async () => {
+    const settings = {throttlingMethod: 'provided'};
+    const context = {settings, computedCache: new Map()};
+    const result = await FirstContentfulPaint.request(
+      {trace, devtoolsLog, settings, TestedAsMobileDevice: true}, 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 659df99e9..a722b5bf8 100644
--- a/lighthouse-core/test/computed/metrics/first-meaningful-paint-test.js
+++ b/lighthouse-core/test/computed/metrics/first-meaningful-paint-test.js
@@ -57,7 +57,7 @@ describe('Metrics: FMP', () => {
     assert.ok(result.pessimisticGraph, 'should have created pessimistic graph');
   });
 
-  it('should compute an observed value', async () => {
+  it('should compute an observed value (desktop)', async () => {
     settings = {throttlingMethod: 'provided'};
     const context = {computedCache: new Map()};
     const result = await FirstMeaningfulPaint.request({trace, devtoolsLog, settings}, context);
@@ -66,6 +66,16 @@ describe('Metrics: FMP', () => {
     assert.equal(result.timestamp, 225414955343);
   });
 
+  it('should compute an observed value (mobile)', async () => {
+    settings = {throttlingMethod: 'provided'};
+    const context = {computedCache: new Map()};
+    const result = await FirstMeaningfulPaint.request(
+      {trace, devtoolsLog, settings, TestedAsMobileDevice: true}, context);
+
+    assert.equal(Math.round(result.timing), 783);
+    assert.equal(result.timestamp, 225414955343);
+  });
+
   it('handles cases when there was a tracingStartedInPage after navStart', async () => {
     trace = lateTracingStartedTrace;
     addEmptyTask();
diff --git a/lighthouse-core/test/computed/metrics/interactive-test.js b/lighthouse-core/test/computed/metrics/interactive-test.js
index 904923f89..7a37d3800 100644
--- a/lighthouse-core/test/computed/metrics/interactive-test.js
+++ b/lighthouse-core/test/computed/metrics/interactive-test.js
@@ -44,7 +44,7 @@ describe('Metrics: TTI', () => {
     assert.ok(result.pessimisticGraph, 'should have created pessimistic graph');
   });
 
-  it('should compute an observed value', async () => {
+  it('should compute an observed value (desktop)', async () => {
     const settings = {throttlingMethod: 'provided'};
     const context = {settings, computedCache: new Map()};
     const result = await Interactive.request({trace, devtoolsLog, settings}, context);
@@ -53,6 +53,16 @@ describe('Metrics: TTI', () => {
     assert.equal(result.timestamp, 225415754204);
   });
 
+  it('should compute an observed value (mobile)', async () => {
+    const settings = {throttlingMethod: 'provided'};
+    const context = {settings, computedCache: new Map()};
+    const result = await Interactive.request(
+      {trace, devtoolsLog, settings, TestedAsMobileDevice: true}, context);
+
+    assert.equal(Math.round(result.timing), 1582);
+    assert.equal(result.timestamp, 225415754204);
+  });
+
   describe('#findOverlappingQuietPeriods', () => {
     it('should return entire range when no activity is present', () => {
       const navigationStart = 220023532;
diff --git a/lighthouse-core/test/computed/metrics/speed-index-test.js b/lighthouse-core/test/computed/metrics/speed-index-test.js
index a1c8c8a55..f64c4c6e4 100644
--- a/lighthouse-core/test/computed/metrics/speed-index-test.js
+++ b/lighthouse-core/test/computed/metrics/speed-index-test.js
@@ -26,7 +26,7 @@ describe('Metrics: Speed Index', () => {
     }).toMatchSnapshot();
   });
 
-  it('should compute an observed value', async () => {
+  it('should compute an observed value (desktop)', async () => {
     const settings = {throttlingMethod: 'provided'};
     const context = {settings, computedCache: new Map()};
     const result = await SpeedIndex.request({trace, devtoolsLog, settings}, context);
@@ -34,4 +34,14 @@ describe('Metrics: Speed Index', () => {
     assert.equal(result.timing, 605);
     assert.equal(result.timestamp, 225414777015);
   });
+
+  it('should compute an observed value (mobile)', async () => {
+    const settings = {throttlingMethod: 'provided'};
+    const context = {settings, computedCache: new Map()};
+    const result = await SpeedIndex.request(
+      {trace, devtoolsLog, settings, TestedAsMobileDevice: true}, context);
+
+    assert.equal(result.timing, 605);
+    assert.equal(result.timestamp, 225414777015);
+  });
 });
diff --git a/lighthouse-core/test/gather/gather-runner-test.js b/lighthouse-core/test/gather/gather-runner-test.js
index 8a504ca04..993352e93 100644
--- a/lighthouse-core/test/gather/gather-runner-test.js
+++ b/lighthouse-core/test/gather/gather-runner-test.js
@@ -234,6 +234,38 @@ describe('GatherRunner', function() {
     });
   });
 
+  describe('collects HostFormFactor as an artifact', () => {
+    const requestedUrl = 'https://example.com';
+
+    function test(name, userAgent, expectedValue) {
+      it(name, async () => {
+        const driver = Object.assign({}, fakeDriver, {
+          getBrowserVersion() {
+            return Promise.resolve({userAgent: userAgent});
+          },
+        });
+        const config = new Config({
+          passes: [],
+          settings: {},
+        });
+        const options = {requestedUrl, driver, settings: config.settings};
+
+        const results = await GatherRunner.run(config.passes, options);
+        expect(results.HostFormFactor).toBe(expectedValue);
+      });
+    }
+
+    /* eslint-disable max-len */
+    const IOS_UA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) OPiOS/10.2.0.93022 Mobile/11D257 Safari/9537.53';
+    const ANDROID_UA = 'Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; SCH-I535 Build/KOT49H) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30';
+    const DESKTOP_UA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36';
+    /* eslint-enable max-len */
+
+    test('works when running on mobile device', IOS_UA, 'mobile');
+    test('works when running on android device', ANDROID_UA, 'mobile');
+    test('works when running on desktop device', DESKTOP_UA, 'desktop');
+  });
+
   it('sets up the driver to begin emulation when all emulation flags are undefined', () => {
     const tests = {
       calledDeviceEmulation: false,
diff --git a/lighthouse-core/test/results/artifacts/artifacts.json b/lighthouse-core/test/results/artifacts/artifacts.json
index be05f36d2..26713ecbc 100644
--- a/lighthouse-core/test/results/artifacts/artifacts.json
+++ b/lighthouse-core/test/results/artifacts/artifacts.json
@@ -1,6 +1,7 @@
 {
   "LighthouseRunWarnings": [],
   "BenchmarkIndex": 1000,
+  "HostFormFactor": "desktop",
   "TestedAsMobileDevice": true,
   "HostUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3358.0 Safari/537.36",
   "NetworkUserAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5 Build/MRA58N) AppleWebKit/537.36(KHTML, like Gecko) Chrome/66.0.3359.30 Mobile Safari/537.36",