Skip to content

Commit

Permalink
core(emulation): refactor emulation settings & CLI flags (#11779)
Browse files Browse the repository at this point in the history
  • Loading branch information
paulirish authored Dec 16, 2020
1 parent ea5afa4 commit a6738e0
Show file tree
Hide file tree
Showing 74 changed files with 769 additions and 411 deletions.
10 changes: 8 additions & 2 deletions clients/devtools-entry.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const lighthouse = require('../lighthouse-core/index.js');
const RawProtocol = require('../lighthouse-core/gather/connections/raw.js');
const log = require('lighthouse-logger');
const {registerLocaleData, lookupLocale} = require('../lighthouse-core/lib/i18n/i18n.js');
const desktopDense4G = require('../lighthouse-core/config/constants.js').throttling.desktopDense4G;
const constants = require('../lighthouse-core/config/constants.js');

/** @typedef {import('../lighthouse-core/gather/connections/connection.js')} Connection */

Expand All @@ -28,9 +28,15 @@ function createConfig(categoryIDs, device) {
/** @type {LH.SharedFlagsSettings} */
const settings = {
onlyCategories: categoryIDs,
// In DevTools, emulation is applied _before_ Lighthouse starts (to deal with viewport emulation bugs). go/xcnjf
// As a result, we don't double-apply viewport emulation.
screenEmulation: {disabled: true},
};
if (device === 'desktop') {
settings.throttling = desktopDense4G;
settings.throttling = constants.throttling.desktopDense4G;
// UA emulation, however, is lost in the protocol handover from devtools frontend to the lighthouse_worker. So it's always applied.
settings.emulatedUserAgent = constants.userAgents.desktop;
settings.formFactor = 'desktop';
}

return {
Expand Down
4 changes: 2 additions & 2 deletions clients/test/lightrider-entry-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ describe('lightrider-entry', () => {
const lrDevice = 'desktop';
await lhBackground.runLighthouseInLR(mockConnection, url, {}, {lrDevice});
const config = runStub.mock.calls[0][1].config;
assert.equal(config.settings.emulatedFormFactor, 'desktop');
assert.equal(config.settings.formFactor, 'desktop');

runStub.mockRestore();
});
Expand All @@ -94,7 +94,7 @@ describe('lightrider-entry', () => {
const lrDevice = 'mobile';
await lhBackground.runLighthouseInLR(mockConnection, url, {}, {lrDevice});
const config = runStub.mock.calls[0][1].config;
assert.equal(config.settings.emulatedFormFactor, 'mobile');
assert.equal(config.settings.formFactor, 'mobile');

runStub.mockRestore();
});
Expand Down
32 changes: 32 additions & 0 deletions docs/emulation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@

# Emulation in Lighthouse

In Lighthouse, "Emulation" refers to the screen/viewport emulation and UserAgent string spoofing.
["Throttling"](./throttling.md) covers the similar topics around network and CPU throttling/simulation.

With the default configuration, Lighthouse emulates a mobile device. There's [a `desktop` configuration](../lighthouse-core/config/desktop-config.js), available to CLI users with `--preset=desktop`, which applies a consistent desktop environment and scoring calibration. This is recommended as a replacement for `--emulated-form-factor=desktop`.

### Advanced emulation setups

Some products use Lighthouse in scenarios where emulation is applied outside of Lighthouse (e.g. by Puppeteer) or running against Chrome on real mobile devices.

You must always set `formFactor`. It doesn't control emulation, but it determines how Lighthouse should interpret the run in regards to scoring performance metrics and skipping mobile-only tests in desktop.

You can choose how `screenEmulation` is applied. It can accept an object of `{width: number, height: number, deviceScaleRatio: number, mobile: boolean, disabled: false}` to apply that screen emulation or an object of `{disabled: true}` if Lighthouse should avoid applying screen emulation. It's typically set to disabled if either emulation is applied outside of Lighthouse, or it's being run on a mobile device. The `mobile` boolean applies overlay scrollbars and a few other mobile-specific screen emulation characteristics.

You can choose how to handle userAgent emulation. The `emulatedUserAgent` property accepts either a `string` to apply the provided userAgent or a `boolean` -- `true` if the default UA spoofing should be applied (default) or `false` if no UA spoofing should be applied. Typically `false` is used if UA spoofing is applied outside of Lighthouse or on a mobile device. You can also redundantly apply userAgent emulation with no risk.

If you're using Lighthouse on a mobile device, you want to set `--screenEmulation.disabled` and `--throttling.cpuSlowdownMultiplier=1`. (`--formFactor=mobile` is the default already).

### Changes made in v7

In Lighthouse v7, most of the configuration regarding emulation changed to be more intuitive and clear. The [tracking issue](https://github.com/GoogleChrome/lighthouse/issues/10910
) captures additional motivations.

* Removed: The `emulatedFormFactor` property (which determined how emulation is applied).
* Removed: The `TestedAsMobileDevice` artifact. Instead of being inferred, the explicit `formFactor` property is used.
* Removed: The `internalDisableDeviceScreenEmulation` property. It's equivalent to the new `--screenEmulation.disabled=true`.
* Added: The `formFactor` property.
* Added: The `screenEmulation` property.
* Added: The `emulatedUserAgent` property.
* (`throttling` and `throttlingMethod` remain unchanged)
3 changes: 1 addition & 2 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,6 @@ The following artifacts are available for use in the audits of Lighthouse plugin
- `RuntimeExceptions`
- `ScriptElements`
- `Stacks`
- `TestedAsMobileDevice`
- `Timing`
- `URL`
- `ViewportDimensions`
Expand Down Expand Up @@ -371,6 +370,6 @@ Most artifacts will try to represent as truthfully as possible what was observed
- [Field Performance](https://github.com/treosh/lighthouse-plugin-field-performance) - A plugin to gather and display Chrome UX Report field data
- [AMP Plugin](https://github.com/ampproject/amp-toolbox/tree/main/packages/lighthouse-plugin-amp) - Runs pages through the AMP validator.
- [Publisher Ads Audits](https://github.com/googleads/pub-ads-lighthouse-plugin) - a well-written, but complex, plugin
- [Green Web Foundation](https://github.com/thegreenwebfoundation/lighthouse-plugin-greenhouse) - A plugin to see which domains run on renewable power.
- [Green Web Foundation](https://github.com/thegreenwebfoundation/lighthouse-plugin-greenhouse) - A plugin to see which domains run on renewable power.
- [requests-content-md5](https://www.npmjs.com/package/lighthouse-plugin-md5) - Generates MD5 hashes from the content of network requests..

4 changes: 2 additions & 2 deletions docs/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ Alternatively, you can instruct Chrome to ignore the invalid certificate by addi
Lighthouse can run against a real mobile device. You can follow the [Remote Debugging on Android (Legacy Workflow)](https://developer.chrome.com/devtools/docs/remote-debugging-legacy) up through step 3.3, but the TL;DR is install & run adb, enable USB debugging, then port forward 9222 from the device to the machine with Lighthouse.
You'll likely want to use the CLI flags `--emulated-form-factor=none --throttling.cpuSlowdownMultiplier=1` to disable any additional emulation.
You'll likely want to use the CLI flags `--screenEmulation.disabled --throttling.cpuSlowdownMultiplier=1` to disable any additional emulation.

```sh
$ adb kill-server
Expand All @@ -128,7 +128,7 @@ $ adb devices -l
$ adb forward tcp:9222 localabstract:chrome_devtools_remote
$ lighthouse --port=9222 --emulated-form-factor=none --throttling.cpuSlowdownMultiplier=1 https://example.com
$ lighthouse --port=9222 --screenEmulation.disabled --throttling.cpuSlowdownMultiplier=1 https://example.com
```

## Lighthouse as trace processor
Expand Down
2 changes: 1 addition & 1 deletion docs/understanding-results.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ An object containing information about the configuration used by Lighthouse.
},
"gatherMode": false,
"disableStorageReset": false,
"emulatedFormFactor": "mobile",
"formFactor": "mobile",
"blockedUrlPatterns": null,
"additionalTraceCategories": null,
"extraHeaders": null,
Expand Down
6 changes: 0 additions & 6 deletions lighthouse-cli/bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,6 @@ async function begin() {
cliFlags.outputPath = 'stdout';
}

// @ts-expect-error - deprecation message for removed disableDeviceEmulation; can remove warning in v6.
if (cliFlags.disableDeviceEmulation) {
log.warn('config', 'The "--disable-device-emulation" has been removed in v5.' +
' Please use "--emulated-form-factor=none" instead.');
}

if (cliFlags.precomputedLanternDataPath) {
const lanternDataStr = fs.readFileSync(cliFlags.precomputedLanternDataPath, 'utf8');
/** @type {LH.PrecomputedLanternData} */
Expand Down
65 changes: 58 additions & 7 deletions lighthouse-cli/cli-flags.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ function getFlags(manualArgv) {
'lighthouse <url> --output=json --output-path=./report.json --save-assets',
'Save trace, screenshots, and named JSON report.')
.example(
'lighthouse <url> --emulated-form-factor=none --throttling-method=provided',
'Disable device emulation and all throttling')
'lighthouse <url> --screenEmulation.disabled --throttling-method=provided --no-emulated-user-agent',
'Disable emulation and all throttling')
.example(
'lighthouse <url> --chrome-flags="--window-size=412,660"',
'Launch Chrome with a specific window size')
Expand Down Expand Up @@ -137,9 +137,18 @@ function getFlags(manualArgv) {
default: 'localhost',
describe: 'The hostname to use for the debugging protocol.',
},
'emulated-form-factor': {
'form-factor': {
type: 'string',
describe: 'Controls the emulated device form factor (mobile vs. desktop) if not disabled',
describe: 'Determines how performance metrics are scored and if mobile-only audits are skipped. For desktop, --preset=desktop instead.',
},
'screenEmulation': {
describe: 'Sets screen emulation parameters. See also --preset. Use --screenEmulation.disabled to disable. Otherwise set these 4 parameters individually: --screenEmulation.mobile --screenEmulation.width=360 --screenEmulation.height=640 --screenEmulation.deviceScaleFactor=2',
coerce: coerceScreenEmulation,
},
'emulatedUserAgent': {
type: 'string',
coerce: coerceOptionalStringBoolean,
describe: 'Sets useragent emulation',
},
'max-wait-for-load': {
type: 'number',
Expand Down Expand Up @@ -185,7 +194,7 @@ function getFlags(manualArgv) {
})
.group([
'save-assets', 'list-all-audits', 'list-trace-categories', 'print-config', 'additional-trace-categories',
'config-path', 'preset', 'chrome-flags', 'port', 'hostname', 'emulated-form-factor',
'config-path', 'preset', 'chrome-flags', 'port', 'hostname', 'form-factor', 'screenEmulation', 'emulatedUserAgent',
'max-wait-for-load', 'enable-error-reporting', 'gather-mode', 'audit-mode',
'only-audits', 'only-categories', 'skip-audits', 'budget-path',
], 'Configuration:')
Expand Down Expand Up @@ -278,9 +287,9 @@ function getFlags(manualArgv) {
})

// Choices added outside of `options()` and cast so tsc picks them up.
.choices('emulated-form-factor', /** @type {['mobile', 'desktop', 'none']} */ (['mobile', 'desktop', 'none']))
.choices('form-factor', /** @type {['mobile', 'desktop']} */ (['mobile', 'desktop']))
.choices('throttling-method', /** @type {['devtools', 'provided', 'simulate']} */ (['devtools', 'provided', 'simulate']))
.choices('preset', /** @type {['perf', 'experimental']} */ (['perf', 'experimental']))
.choices('preset', /** @type {['perf', 'experimental', 'desktop']} */ (['perf', 'experimental', 'desktop']))

.check(argv => {
// Lighthouse doesn't need a URL if...
Expand Down Expand Up @@ -418,6 +427,48 @@ function coerceThrottling(value) {
return throttlingSettings;
}

/**
* Take yarg's unchecked object value and ensure it is a proper LH.screenEmulationSettings.
* @param {unknown} value
* @return {Partial<LH.ScreenEmulationSettings>}
*/
function coerceScreenEmulation(value) {
if (!isObjectOfUnknownValues(value)) {
throw new Error(`Invalid value: Argument 'screenEmulation' must be an object, specified per-property ('screenEmulation.width', 'screenEmulation.deviceScaleFactor', etc)`);
}

/** @type {Array<keyof LH.ScreenEmulationSettings>} */
const keys = ['width', 'height', 'deviceScaleFactor', 'mobile', 'disabled'];
/** @type {Partial<LH.ScreenEmulationSettings>} */
const screenEmulationSettings = {};

for (const key of keys) {
const possibleSetting = value[key];
switch (key) {
case 'width':
case 'height':
case 'deviceScaleFactor':
if (possibleSetting !== undefined && typeof possibleSetting !== 'number') {
throw new Error(`Invalid value: 'screenEmulation.${key}' must be a number`);
}
screenEmulationSettings[key] = possibleSetting;

break;
case 'mobile':
case 'disabled':
if (possibleSetting !== undefined && typeof possibleSetting !== 'boolean') {
throw new Error(`Invalid value: 'screenEmulation.${key}' must be a boolean`);
}
screenEmulationSettings[key] = possibleSetting;
break;
default:
throw new Error(`Unrecognized screenEmulation option: ${key}`);
}
}

return screenEmulationSettings;
}

module.exports = {
getFlags,
};
22 changes: 18 additions & 4 deletions lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1485,10 +1485,10 @@ Object {
"budgets": null,
"channel": "cli",
"disableStorageReset": false,
"emulatedFormFactor": "mobile",
"emulatedUserAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4143.7 Mobile Safari/537.36 Chrome-Lighthouse",
"extraHeaders": null,
"formFactor": "mobile",
"gatherMode": false,
"internalDisableDeviceScreenEmulation": false,
"locale": "en-US",
"maxWaitForFcp": 30000,
"maxWaitForLoad": 45000,
Expand All @@ -1498,6 +1498,13 @@ Object {
"html",
],
"precomputedLanternData": null,
"screenEmulation": Object {
"deviceScaleFactor": 2.625,
"disabled": false,
"height": 640,
"mobile": true,
"width": 360,
},
"skipAudits": null,
"throttling": Object {
"cpuSlowdownMultiplier": 4,
Expand Down Expand Up @@ -1631,10 +1638,10 @@ Object {
"budgets": null,
"channel": "cli",
"disableStorageReset": false,
"emulatedFormFactor": "mobile",
"emulatedUserAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4143.7 Mobile Safari/537.36 Chrome-Lighthouse",
"extraHeaders": null,
"formFactor": "mobile",
"gatherMode": false,
"internalDisableDeviceScreenEmulation": false,
"locale": "en-US",
"maxWaitForFcp": 30000,
"maxWaitForLoad": 45000,
Expand All @@ -1646,6 +1653,13 @@ Object {
"json",
],
"precomputedLanternData": null,
"screenEmulation": Object {
"deviceScaleFactor": 2.625,
"disabled": false,
"height": 640,
"mobile": true,
"width": 360,
},
"skipAudits": null,
"throttling": Object {
"cpuSlowdownMultiplier": 4,
Expand Down
39 changes: 39 additions & 0 deletions lighthouse-cli/test/cli/index-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,43 @@ describe('CLI Tests', function() {
expect(config).toMatchSnapshot();
});
});

describe('preset', () => {
it('desktop should set appropriate config', () => {
const ret = spawnSync('node', [indexPath, '--print-config', '--preset=desktop'], {
encoding: 'utf8',
});

const config = JSON.parse(ret.stdout);
const {emulatedUserAgent, formFactor, screenEmulation, throttling, throttlingMethod} =
config.settings;
const emulationSettings =
{emulatedUserAgent, formFactor, screenEmulation, throttling, throttlingMethod};

/* eslint-disable max-len */
expect(emulationSettings).toMatchInlineSnapshot(`
Object {
"emulatedUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4143.7 Safari/537.36 Chrome-Lighthouse",
"formFactor": "desktop",
"screenEmulation": Object {
"deviceScaleFactor": 1,
"disabled": false,
"height": 940,
"mobile": false,
"width": 1350,
},
"throttling": Object {
"cpuSlowdownMultiplier": 1,
"downloadThroughputKbps": 0,
"requestLatencyMs": 0,
"rttMs": 40,
"throughputKbps": 10240,
"uploadThroughputKbps": 0,
},
"throttlingMethod": "simulate",
}
`);
/* eslint-enable max-len */
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ const expectations = [
{
artifacts: {
HostFormFactor: 'desktop',
TestedAsMobileDevice: true,
Stacks: [{
id: 'jquery',
}, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
module.exports = {
extends: 'lighthouse:default',
settings: {
emulatedFormFactor: 'desktop',
formFactor: 'desktop',
screenEmulation: {
width: 1024,
height: 768,
deviceScaleFactor: 1,
mobile: false,
disabled: false,
},
},
};
8 changes: 4 additions & 4 deletions lighthouse-core/audits/content-width.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,21 @@ class ContentWidth extends Audit {
title: str_(UIStrings.title),
failureTitle: str_(UIStrings.failureTitle),
description: str_(UIStrings.description),
requiredArtifacts: ['ViewportDimensions', 'TestedAsMobileDevice'],
requiredArtifacts: ['ViewportDimensions'],
};
}

/**
* @param {LH.Artifacts} artifacts
* @param {LH.Audit.Context} context
* @return {LH.Audit.Product}
*/
static audit(artifacts) {
const IsMobile = artifacts.TestedAsMobileDevice;
static audit(artifacts, context) {
const viewportWidth = artifacts.ViewportDimensions.innerWidth;
const windowWidth = artifacts.ViewportDimensions.outerWidth;
const widthsMatch = viewportWidth === windowWidth;

if (!IsMobile) {
if (context.settings.formFactor === 'desktop') {
return {
score: 1,
notApplicable: true,
Expand Down
Loading

0 comments on commit a6738e0

Please sign in to comment.