core(fr): prepare emulation utilities for shared use (#12375)

This commit is contained in:
Patrick Hulce 2021-04-21 12:55:38 -05:00 коммит произвёл GitHub
Родитель 8c0ffa3b77
Коммит f57ac8c57c
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
8 изменённых файлов: 162 добавлений и 114 удалений

Просмотреть файл

@ -8,7 +8,6 @@
const Fetcher = require('./fetcher.js');
const ExecutionContext = require('./driver/execution-context.js');
const {waitForFullyLoaded, waitForFrameNavigated} = require('./driver/wait-for-condition.js');
const emulation = require('../lib/emulation.js');
const LHElement = require('../lib/lh-element.js');
const LHError = require('../lib/lh-error.js');
const NetworkRequest = require('../lib/network-request.js');
@ -762,35 +761,6 @@ class Driver {
await this.sendCommand('Debugger.setAsyncCallStackDepth', {maxDepth: 8});
}
/**
* @param {LH.Config.Settings} settings
* @return {Promise<void>}
*/
async beginEmulation(settings) {
await emulation.emulate(this, settings);
await this.setThrottling(settings, {useThrottling: true});
}
/**
* @param {LH.Config.Settings} settings
* @param {{useThrottling?: boolean}} passConfig
* @return {Promise<void>}
*/
async setThrottling(settings, passConfig) {
if (settings.throttlingMethod !== 'devtools') {
return emulation.clearAllNetworkEmulation(this);
}
const cpuPromise = passConfig.useThrottling ?
emulation.enableCPUThrottling(this, settings.throttling) :
emulation.disableCPUThrottling(this);
const networkPromise = passConfig.useThrottling ?
emulation.enableNetworkThrottling(this, settings.throttling) :
emulation.clearAllNetworkEmulation(this);
await Promise.all([cpuPromise, networkPromise]);
}
/**
* Clear the network cache on disk and in memory.
* @return {Promise<void>}

Просмотреть файл

@ -9,6 +9,7 @@ const log = require('lighthouse-logger');
const LHError = require('../lib/lh-error.js');
const NetworkAnalyzer = require('../lib/dependency-graph/simulator/network-analyzer.js');
const NetworkRecorder = require('../lib/network-recorder.js');
const emulation = require('../lib/emulation.js');
const constants = require('../config/constants.js');
const i18n = require('../lib/i18n/i18n.js');
const URL = require('../lib/url-shim.js');
@ -129,7 +130,7 @@ class GatherRunner {
log.time(status);
const resetStorage = !options.settings.disableStorageReset;
await driver.assertNoSameOriginServiceWorkerClients(options.requestedUrl);
await driver.beginEmulation(options.settings);
await emulation.emulate(driver.defaultSession, options.settings);
await driver.enableRuntimeEvents();
await driver.enableAsyncStacks();
await driver.cacheNatives();
@ -344,8 +345,10 @@ class GatherRunner {
const status = {msg: 'Setting up network for the pass trace', id: `lh:gather:setupPassNetwork`};
log.time(status);
const session = passContext.driver.defaultSession;
const passConfig = passContext.passConfig;
await passContext.driver.setThrottling(passContext.settings, passConfig);
if (passConfig.useThrottling) await emulation.throttle(session, passContext.settings);
else await emulation.clearThrottling(session);
const blockedUrls = (passContext.passConfig.blockedUrlPatterns || [])
.concat(passContext.settings.blockedUrlPatterns || []);
@ -752,7 +755,7 @@ class GatherRunner {
const loadData = await GatherRunner.endRecording(passContext);
// Disable throttling so the afterPass analysis isn't throttled
await driver.setThrottling(passContext.settings, {useThrottling: false});
await emulation.clearThrottling(driver.defaultSession);
// In case of load error, save log and trace with an error prefix, return no artifacts for this pass.
const pageLoadError = GatherRunner.getPageLoadError(passContext, loadData, possibleNavError);

Просмотреть файл

@ -8,6 +8,7 @@
/* globals window document getBoundingClientRect */
const Gatherer = require('./gatherer.js');
const emulation = require('../../lib/emulation.js');
const pageFunctions = require('../../lib/page-functions.js');
/** @typedef {import('../driver.js')} Driver */
@ -147,7 +148,7 @@ class FullPageScreenshot extends Gatherer {
} finally {
// Revert resized page.
if (lighthouseControlsEmulation) {
await driver.beginEmulation(passContext.settings);
await emulation.emulate(driver.defaultSession, passContext.settings);
} else {
// Best effort to reset emulation to what it was.
// https://github.com/GoogleChrome/lighthouse/pull/10716#discussion_r428970681

Просмотреть файл

@ -5,8 +5,6 @@
*/
'use strict';
/** @typedef {import('../gather/driver.js')} Driver */
const NO_THROTTLING_METRICS = {
latency: 0,
downloadThroughput: 0,
@ -19,16 +17,15 @@ const NO_CPU_THROTTLE_METRICS = {
};
/**
*
* @param {Driver} driver
* @param {LH.Gatherer.FRProtocolSession} session
* @param {LH.Config.Settings} settings
* @return {Promise<void>}
*/
async function emulate(driver, settings) {
async function emulate(session, settings) {
if (settings.emulatedUserAgent !== false) {
// Network.enable must be called for UA overriding to work
await driver.sendCommand('Network.enable');
await driver.sendCommand('Network.setUserAgentOverride', {
await session.sendCommand('Network.enable');
await session.sendCommand('Network.setUserAgentOverride', {
userAgent: /** @type {string} */ (settings.emulatedUserAgent),
});
}
@ -36,20 +33,45 @@ async function emulate(driver, settings) {
if (settings.screenEmulation.disabled !== true) {
const {width, height, deviceScaleFactor, mobile} = settings.screenEmulation;
const params = {width, height, deviceScaleFactor, mobile};
await driver.sendCommand('Emulation.setDeviceMetricsOverride', params);
await driver.sendCommand('Emulation.setTouchEmulationEnabled', {
await session.sendCommand('Emulation.setDeviceMetricsOverride', params);
await session.sendCommand('Emulation.setTouchEmulationEnabled', {
enabled: params.mobile,
});
}
}
/**
* Sets the throttling options specified in config settings, clearing existing network throttling if
* throttlingMethod is not `devtools` (but not CPU throttling, suspected requirement of WPT-compat).
*
* @param {LH.Gatherer.FRProtocolSession} session
* @param {LH.Config.Settings} settings
* @return {Promise<void>}
*/
async function throttle(session, settings) {
// TODO(FR-COMPAT): reconsider if this should be resetting anything
if (settings.throttlingMethod !== 'devtools') return clearNetworkThrottling(session);
await Promise.all([
enableNetworkThrottling(session, settings.throttling),
enableCPUThrottling(session, settings.throttling),
]);
}
/**
* @param {Driver} driver
* @param {LH.Gatherer.FRProtocolSession} session
* @return {Promise<void>}
*/
async function clearThrottling(session) {
await Promise.all([clearNetworkThrottling(session), clearCPUThrottling(session)]);
}
/**
* @param {LH.Gatherer.FRProtocolSession} session
* @param {Required<LH.ThrottlingSettings>} throttlingSettings
* @return {Promise<void>}
*/
function enableNetworkThrottling(driver, throttlingSettings) {
function enableNetworkThrottling(session, throttlingSettings) {
/** @type {LH.Crdp.Network.EmulateNetworkConditionsRequest} */
const conditions = {
offline: false,
@ -61,39 +83,41 @@ function enableNetworkThrottling(driver, throttlingSettings) {
// DevTools expects throughput in bytes per second rather than kbps
conditions.downloadThroughput = Math.floor(conditions.downloadThroughput * 1024 / 8);
conditions.uploadThroughput = Math.floor(conditions.uploadThroughput * 1024 / 8);
return driver.sendCommand('Network.emulateNetworkConditions', conditions);
return session.sendCommand('Network.emulateNetworkConditions', conditions);
}
/**
* @param {Driver} driver
* @param {LH.Gatherer.FRProtocolSession} session
* @return {Promise<void>}
*/
function clearAllNetworkEmulation(driver) {
return driver.sendCommand('Network.emulateNetworkConditions', NO_THROTTLING_METRICS);
function clearNetworkThrottling(session) {
return session.sendCommand('Network.emulateNetworkConditions', NO_THROTTLING_METRICS);
}
/**
* @param {Driver} driver
* @param {LH.Gatherer.FRProtocolSession} session
* @param {Required<LH.ThrottlingSettings>} throttlingSettings
* @return {Promise<void>}
*/
function enableCPUThrottling(driver, throttlingSettings) {
function enableCPUThrottling(session, throttlingSettings) {
const rate = throttlingSettings.cpuSlowdownMultiplier;
return driver.sendCommand('Emulation.setCPUThrottlingRate', {rate});
return session.sendCommand('Emulation.setCPUThrottlingRate', {rate});
}
/**
* @param {Driver} driver
* @param {LH.Gatherer.FRProtocolSession} session
* @return {Promise<void>}
*/
function disableCPUThrottling(driver) {
return driver.sendCommand('Emulation.setCPUThrottlingRate', NO_CPU_THROTTLE_METRICS);
function clearCPUThrottling(session) {
return session.sendCommand('Emulation.setCPUThrottlingRate', NO_CPU_THROTTLE_METRICS);
}
module.exports = {
emulate,
throttle,
clearThrottling,
enableNetworkThrottling,
clearAllNetworkEmulation,
clearNetworkThrottling,
enableCPUThrottling,
disableCPUThrottling,
clearCPUThrottling,
};

Просмотреть файл

@ -36,12 +36,6 @@ function makeFakeDriver({protocolGetVersionResponse}) {
gotoURL(url) {
return Promise.resolve({finalUrl: url, timedOut: false});
},
beginEmulation() {
return Promise.resolve();
},
setThrottling() {
return Promise.resolve();
},
dismissJavaScriptDialogs() {
return Promise.resolve();
},

Просмотреть файл

@ -129,6 +129,17 @@ function resetDefaultMockResponses() {
.mockResponse('ServiceWorker.enable');
}
/**
* Restore the emulation to its original implementation for testing emulation-sensitive logic.
*/
function restoreActualEmulation() {
const actualEmulation = jest.requireActual('../../lib/emulation.js');
const emulation = require('../../lib/emulation.js');
emulation.emulate = actualEmulation.emulate;
emulation.throttle = actualEmulation.throttle;
emulation.clearThrottling = actualEmulation.clearThrottling;
}
beforeEach(() => {
// @ts-expect-error - connectionStub has a mocked version of sendCommand implemented in each test
connectionStub = new Connection();
@ -138,6 +149,11 @@ beforeEach(() => {
};
driver = new EmulationDriver(connectionStub);
resetDefaultMockResponses();
const emulation = require('../../lib/emulation.js');
emulation.emulate = jest.fn();
emulation.throttle = jest.fn();
emulation.clearThrottling = jest.fn();
});
describe('GatherRunner', function() {
@ -275,17 +291,14 @@ describe('GatherRunner', function() {
});
it('sets up the driver to begin emulation when all flags are undefined', async () => {
restoreActualEmulation();
await GatherRunner.setupDriver(driver, {settings: getSettings('mobile')});
connectionStub.sendCommand.findInvocation('Emulation.setDeviceMetricsOverride');
expect(connectionStub.sendCommand.findInvocation('Network.emulateNetworkConditions')).toEqual({
latency: 0, downloadThroughput: 0, uploadThroughput: 0, offline: false,
});
expect(() =>
connectionStub.sendCommand.findInvocation('Emulation.setCPUThrottlingRate')).toThrow();
});
it('applies the correct emulation given a particular formFactor', async () => {
restoreActualEmulation();
await GatherRunner.setupDriver(driver, {settings: getSettings('mobile')});
expect(connectionStub.sendCommand.findInvocation('Emulation.setDeviceMetricsOverride'))
.toMatchObject({mobile: true});
@ -296,30 +309,6 @@ describe('GatherRunner', function() {
.toMatchObject({mobile: false});
});
it('sets throttling according to settings', async () => {
await GatherRunner.setupDriver(driver, {
settings: {
formFactor: 'mobile',
screenEmulation: constants.screenEmulationMetrics.mobile,
throttlingMethod: 'devtools',
throttling: {
requestLatencyMs: 100,
downloadThroughputKbps: 8,
uploadThroughputKbps: 8,
cpuSlowdownMultiplier: 1,
},
},
});
connectionStub.sendCommand.findInvocation('Emulation.setDeviceMetricsOverride');
expect(connectionStub.sendCommand.findInvocation('Network.emulateNetworkConditions')).toEqual({
latency: 100, downloadThroughput: 1024, uploadThroughput: 1024, offline: false,
});
expect(connectionStub.sendCommand.findInvocation('Emulation.setCPUThrottlingRate')).toEqual({
rate: 1,
});
});
it('clears origin storage', () => {
const asyncFunc = () => Promise.resolve();
/** @type {Record<string, boolean>} */
@ -334,8 +323,6 @@ describe('GatherRunner', function() {
};
const driver = {
assertNoSameOriginServiceWorkerClients: asyncFunc,
beginEmulation: asyncFunc,
setThrottling: asyncFunc,
dismissJavaScriptDialogs: asyncFunc,
enableRuntimeEvents: asyncFunc,
enableAsyncStacks: asyncFunc,
@ -371,7 +358,6 @@ describe('GatherRunner', function() {
beginTrace: asyncFunc,
gotoURL: async () => ({}),
cleanBrowserCaches: createCheck('calledCleanBrowserCaches'),
setThrottling: asyncFunc,
blockUrlPatterns: asyncFunc,
setExtraHTTPHeaders: asyncFunc,
endTrace: asyncFunc,
@ -525,8 +511,6 @@ describe('GatherRunner', function() {
};
const driver = {
assertNoSameOriginServiceWorkerClients: asyncFunc,
beginEmulation: asyncFunc,
setThrottling: asyncFunc,
dismissJavaScriptDialogs: asyncFunc,
enableRuntimeEvents: asyncFunc,
enableAsyncStacks: asyncFunc,
@ -548,6 +532,64 @@ describe('GatherRunner', function() {
});
});
it('sets throttling appropriately', async () => {
restoreActualEmulation();
await GatherRunner.setupPassNetwork({
driver,
settings: {
formFactor: 'mobile',
screenEmulation: constants.screenEmulationMetrics.mobile,
throttlingMethod: 'devtools',
throttling: {
requestLatencyMs: 100,
downloadThroughputKbps: 8,
uploadThroughputKbps: 8,
cpuSlowdownMultiplier: 2,
},
},
passConfig: {
useThrottling: true,
gatherers: [],
},
});
expect(connectionStub.sendCommand.findInvocation('Network.emulateNetworkConditions')).toEqual({
latency: 100, downloadThroughput: 1024, uploadThroughput: 1024, offline: false,
});
expect(connectionStub.sendCommand.findInvocation('Emulation.setCPUThrottlingRate')).toEqual({
rate: 2,
});
});
it('clears throttling when useThrottling=false', async () => {
restoreActualEmulation();
await GatherRunner.setupPassNetwork({
driver,
settings: {
formFactor: 'mobile',
screenEmulation: constants.screenEmulationMetrics.mobile,
throttlingMethod: 'devtools',
throttling: {
requestLatencyMs: 100,
downloadThroughputKbps: 8,
uploadThroughputKbps: 8,
cpuSlowdownMultiplier: 2,
},
},
passConfig: {
useThrottling: false,
gatherers: [],
},
});
expect(connectionStub.sendCommand.findInvocation('Network.emulateNetworkConditions')).toEqual({
latency: 0, downloadThroughput: 0, uploadThroughput: 0, offline: false,
});
expect(connectionStub.sendCommand.findInvocation('Emulation.setCPUThrottlingRate')).toEqual({
rate: 1,
});
});
it('tells the driver to block given URL patterns when blockedUrlPatterns is given', async () => {
await GatherRunner.setupPassNetwork({
driver,

Просмотреть файл

@ -7,6 +7,9 @@
/* eslint-env jest */
let mockEmulate = jest.fn();
jest.mock('../../../lib/emulation.js', () => ({emulate: (...args) => mockEmulate(...args)}));
const FullPageScreenshotGatherer = require('../../../gather/gatherers/full-page-screenshot.js');
// Headless's default value is (1024 * 16), but this varies by device
@ -16,6 +19,21 @@ const maxTextureSizeMock = 1024 * 8;
* @param {{contentSize: {width: number, height: number}, screenSize: {width?: number, height?: number, dpr: number}, screenshotData: string[]}}
*/
function createMockDriver({contentSize, screenSize, screenshotData}) {
const sendCommand = jest.fn().mockImplementation(method => {
if (method === 'Page.getLayoutMetrics') {
return {
contentSize,
// See comment within _takeScreenshot() implementation
layoutViewport: {clientWidth: contentSize.width, clientHeight: contentSize.height},
};
}
if (method === 'Page.captureScreenshot') {
return {
data: screenshotData && screenshotData.length ? screenshotData.shift() : 'abc',
};
}
});
return {
executionContext: {
evaluate: async function(fn) {
@ -40,25 +58,16 @@ function createMockDriver({contentSize, screenSize, screenshotData}) {
}
},
},
beginEmulation: jest.fn(),
sendCommand: jest.fn().mockImplementation(method => {
if (method === 'Page.getLayoutMetrics') {
return {
contentSize,
// See comment within _takeScreenshot() implementation
layoutViewport: {clientWidth: contentSize.width, clientHeight: contentSize.height},
};
}
if (method === 'Page.captureScreenshot') {
return {
data: screenshotData && screenshotData.length ? screenshotData.shift() : 'abc',
};
}
}),
sendCommand,
defaultSession: {sendCommand},
};
}
describe('FullPageScreenshot gatherer', () => {
beforeEach(() => {
mockEmulate = jest.fn();
});
it('captures a full-page screenshot', async () => {
const fpsGatherer = new FullPageScreenshotGatherer();
const driver = createMockDriver({
@ -113,8 +122,8 @@ describe('FullPageScreenshot gatherer', () => {
await fpsGatherer.afterPass(passContext);
const expectedArgs = {formFactor: 'mobile', screenEmulation: {disabled: false, mobile: true}};
expect(driver.beginEmulation).toHaveBeenCalledWith(expectedArgs);
expect(driver.beginEmulation).toHaveBeenCalledTimes(1);
expect(mockEmulate).toHaveBeenCalledTimes(1);
expect(mockEmulate).toHaveBeenCalledWith(driver.defaultSession, expectedArgs);
});
it('resets the emulation correctly when Lighthouse does not control it', async () => {

Просмотреть файл

@ -235,6 +235,11 @@ function makeMocksForGatherRunner() {
jest.mock('../gather/gatherers/web-app-manifest.js', () => ({
getWebAppManifest: async () => null,
}));
jest.mock('../lib/emulation.js', () => ({
emulate: jest.fn(),
throttle: jest.fn(),
clearThrottling: jest.fn(),
}));
}
module.exports = {