Skip to content

Commit

Permalink
core(fr): setup emulation and throttling for timespans (#13058)
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickhulce authored Sep 14, 2021
1 parent f4d37a9 commit ad3d41c
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 35 deletions.
2 changes: 2 additions & 0 deletions lighthouse-core/fraggle-rock/gather/timespan-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const {
collectPhaseArtifacts,
awaitArtifacts,
} = require('./runner-helpers.js');
const {prepareTargetForTimespanMode} = require('../../gather/driver/prepare.js');
const {initializeConfig} = require('../config/config.js');
const {getBaseArtifacts, finalizeArtifacts} = require('./base-artifacts.js');

Expand Down Expand Up @@ -42,6 +43,7 @@ async function startTimespan(options) {
settings: config.settings,
};

await prepareTargetForTimespanMode(driver, config.settings);
await collectPhaseArtifacts({phase: 'startInstrumentation', ...phaseOptions});
await collectPhaseArtifacts({phase: 'startSensitiveInstrumentation', ...phaseOptions});

Expand Down
60 changes: 43 additions & 17 deletions lighthouse-core/gather/driver/prepare.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,25 +88,25 @@ async function resetStorageForNavigation(session, navigation) {
}

/**
* Prepares a target for a particular navigation by resetting storage and setting throttling.
* Prepares a target for observational analysis by setting throttling and network headers/blocked patterns.
*
* This method assumes `prepareTargetForNavigationMode` has already been invoked.
* This method assumes `prepareTargetForNavigationMode` or `prepareTargetForTimespanMode` has already been invoked.
*
* @param {LH.Gatherer.FRProtocolSession} session
* @param {LH.Config.Settings} settings
* @param {Pick<LH.Config.NavigationDefn, 'disableThrottling'|'disableStorageReset'|'blockedUrlPatterns'> & {url: string}} navigation
* @param {{disableThrottling: boolean, blockedUrlPatterns?: string[]}} options
*/
async function prepareNetworkForNavigation(session, settings, navigation) {
const status = {msg: 'Preparing network conditions', id: `lh:gather:prepareNetworkForNavigation`};
async function prepareThrottlingAndNetwork(session, settings, options) {
const status = {msg: 'Preparing network conditions', id: `lh:gather:prepareThrottlingAndNetwork`};
log.time(status);

if (navigation.disableThrottling) await emulation.clearThrottling(session);
if (options.disableThrottling) await emulation.clearThrottling(session);
else await emulation.throttle(session, settings);

// Set request blocking before any network activity.
// No "clearing" is done at the end of the navigation since Network.setBlockedURLs([]) will unset all if
// neccessary at the beginning of the next navigation.
const blockedUrls = (navigation.blockedUrlPatterns || []).concat(
// No "clearing" is done at the end of the recording since Network.setBlockedURLs([]) will unset all if
// neccessary at the beginning of the next section.
const blockedUrls = (options.blockedUrlPatterns || []).concat(
settings.blockedUrlPatterns || []
);
await session.sendCommand('Network.setBlockedURLs', {urls: blockedUrls});
Expand All @@ -118,23 +118,48 @@ async function prepareNetworkForNavigation(session, settings, navigation) {
}

/**
* Prepares a target to be analyzed in navigation mode by enabling protocol domains, emulation, and new document
* handlers for global APIs or error handling.
*
* This method should be used in combination with `prepareTargetForIndividualNavigation` before a specific navigation occurs.
* Prepares a target to be analyzed by setting up device emulation (screen/UA, not throttling) and
* async stack traces for network initiators.
*
* @param {LH.Gatherer.FRTransitionalDriver} driver
* @param {LH.Config.Settings} settings
*/
async function prepareTargetForNavigationMode(driver, settings) {
// Enable network domain here so future calls to `emulate()` don't clear cache (#12631)
async function prepareDeviceEmulationAndAsyncStacks(driver, settings) {
// Enable network domain here so future calls to `emulate()` don't clear cache (https://github.com/GoogleChrome/lighthouse/issues/12631)
await driver.defaultSession.sendCommand('Network.enable');

// Emulate our target device screen and user agent.
await emulation.emulate(driver.defaultSession, settings);

// Enable better stacks on network requests.
await enableAsyncStacks(driver.defaultSession);
}

/**
* Prepares a target to be analyzed in timespan mode by enabling protocol domains, emulation, and throttling.
*
* @param {LH.Gatherer.FRTransitionalDriver} driver
* @param {LH.Config.Settings} settings
*/
async function prepareTargetForTimespanMode(driver, settings) {
await prepareDeviceEmulationAndAsyncStacks(driver, settings);
await prepareThrottlingAndNetwork(driver.defaultSession, settings, {
disableThrottling: false,
blockedUrlPatterns: undefined,
});
}

/**
* Prepares a target to be analyzed in navigation mode by enabling protocol domains, emulation, and new document
* handlers for global APIs or error handling.
*
* This method should be used in combination with `prepareTargetForIndividualNavigation` before a specific navigation occurs.
*
* @param {LH.Gatherer.FRTransitionalDriver} driver
* @param {LH.Config.Settings} settings
*/
async function prepareTargetForNavigationMode(driver, settings) {
await prepareDeviceEmulationAndAsyncStacks(driver, settings);

// Automatically handle any JavaScript dialogs to prevent a hung renderer.
await dismissJavaScriptDialogs(driver.defaultSession);
Expand Down Expand Up @@ -168,13 +193,14 @@ async function prepareTargetForIndividualNavigation(session, settings, navigatio
warnings.push(...storageWarnings);
}

await prepareNetworkForNavigation(session, settings, navigation);
await prepareThrottlingAndNetwork(session, settings, navigation);

return {warnings};
}

module.exports = {
prepareNetworkForNavigation,
prepareThrottlingAndNetwork,
prepareTargetForTimespanMode,
prepareTargetForNavigationMode,
prepareTargetForIndividualNavigation,
};
9 changes: 5 additions & 4 deletions lighthouse-core/test/fraggle-rock/api-test-pptr.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ jest.setTimeout(90_000);
* Some audits can be notApplicable based on machine timing information.
* Exclude these audits from applicability comparisons. */
const FLAKY_AUDIT_IDS_APPLICABILITY = new Set([
'long-tasks',
'screenshot-thumbnails',
'long-tasks', // Depends on whether the longest task takes <50ms.
'screenshot-thumbnails', // Depends on OS whether frames happen to be generated on non-visual timespan changes.
'layout-shift-elements', // Depends on if the JS takes too long after input to be ignored for layout shift.
]);

/**
Expand Down Expand Up @@ -156,7 +157,7 @@ describe('Fraggle Rock API', () => {
// TODO(FR-COMPAT): This assertion can be removed when full compatibility is reached.
expect(auditResults.length).toMatchInlineSnapshot(`48`);

expect(notApplicableAudits.length).toMatchInlineSnapshot(`6`);
expect(notApplicableAudits.length).toMatchInlineSnapshot(`5`);
expect(notApplicableAudits.map(audit => audit.id)).not.toContain('server-response-time');
expect(notApplicableAudits.map(audit => audit.id)).not.toContain('total-blocking-time');

Expand Down Expand Up @@ -199,7 +200,7 @@ describe('Fraggle Rock API', () => {
const {auditResults, erroredAudits, notApplicableAudits} = getAuditsBreakdown(result.lhr);
expect(auditResults.length).toMatchInlineSnapshot(`48`);

expect(notApplicableAudits.length).toMatchInlineSnapshot(`20`);
expect(notApplicableAudits.length).toMatchInlineSnapshot(`19`);
expect(notApplicableAudits.map(audit => audit.id)).toContain('server-response-time');
expect(notApplicableAudits.map(audit => audit.id)).not.toContain('total-blocking-time');

Expand Down
5 changes: 5 additions & 0 deletions lighthouse-core/test/fraggle-rock/gather/mock-driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ function createMockContext() {
function mockDriverSubmodules() {
const navigationMock = {gotoURL: jest.fn()};
const prepareMock = {
prepareThrottlingAndNetwork: jest.fn(),
prepareTargetForTimespanMode: jest.fn(),
prepareTargetForNavigationMode: jest.fn(),
prepareTargetForIndividualNavigation: jest.fn(),
};
Expand All @@ -233,6 +235,8 @@ function mockDriverSubmodules() {

function reset() {
navigationMock.gotoURL = jest.fn().mockResolvedValue({finalUrl: 'https://example.com', warnings: [], timedOut: false});
prepareMock.prepareThrottlingAndNetwork = jest.fn().mockResolvedValue(undefined);
prepareMock.prepareTargetForTimespanMode = jest.fn().mockResolvedValue(undefined);
prepareMock.prepareTargetForNavigationMode = jest.fn().mockResolvedValue({warnings: []});
prepareMock.prepareTargetForIndividualNavigation = jest.fn().mockResolvedValue({warnings: []});
storageMock.clearDataForOrigin = jest.fn();
Expand All @@ -248,6 +252,7 @@ function mockDriverSubmodules() {
* @return {(...args: any[]) => void}
*/
const get = (target, name) => {
if (!target[name]) throw new Error(`Target does not have property "${name}"`);
return (...args) => target[name](...args);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const {
createMockDriver,
createMockPage,
createMockGathererInstance,
mockDriverSubmodules,
mockDriverModule,
mockRunnerModule,
} = require('./mock-driver.js');
Expand All @@ -19,6 +20,7 @@ const {
let mockRunnerRun = jest.fn();
/** @type {ReturnType<typeof createMockDriver>} */
let mockDriver;
const mockSubmodules = mockDriverSubmodules();

jest.mock('../../../runner.js', () => mockRunnerModule(() => mockRunnerRun));
jest.mock('../../../fraggle-rock/gather/driver.js', () =>
Expand All @@ -40,6 +42,7 @@ describe('Timespan Runner', () => {
let config;

beforeEach(() => {
mockSubmodules.reset();
mockPage = createMockPage();
mockDriver = createMockDriver();
mockRunnerRun = jest.fn();
Expand Down Expand Up @@ -71,6 +74,12 @@ describe('Timespan Runner', () => {
expect(mockRunnerRun).toHaveBeenCalled();
});

it('should prepare the target', async () => {
const timespan = await startTimespan({page, config});
expect(mockSubmodules.prepareMock.prepareTargetForTimespanMode).toHaveBeenCalled();
await timespan.endTimespan();
});

it('should invoke startInstrumentation', async () => {
const timespan = await startTimespan({page, config});
expect(gathererA.startInstrumentation).toHaveBeenCalled();
Expand Down
102 changes: 88 additions & 14 deletions lighthouse-core/test/gather/driver/prepare-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ afterEach(() => {
jest.useRealTimers();
});

describe('.prepareNetworkForNavigation()', () => {
describe('.prepareThrottlingAndNetwork()', () => {
it('sets throttling appropriately', async () => {
await prepare.prepareNetworkForNavigation(
await prepare.prepareThrottlingAndNetwork(
sessionMock.asSession(),
{
...constants.defaultSettings,
Expand All @@ -53,10 +53,7 @@ describe('.prepareNetworkForNavigation()', () => {
cpuSlowdownMultiplier: 2,
},
},
{
...constants.defaultNavigationConfig,
url,
}
constants.defaultNavigationConfig
);

expect(sessionMock.sendCommand.findInvocation('Network.emulateNetworkConditions')).toEqual({
Expand All @@ -71,7 +68,7 @@ describe('.prepareNetworkForNavigation()', () => {
});

it('disables throttling', async () => {
await prepare.prepareNetworkForNavigation(
await prepare.prepareThrottlingAndNetwork(
sessionMock.asSession(),
{
...constants.defaultSettings,
Expand All @@ -86,7 +83,6 @@ describe('.prepareNetworkForNavigation()', () => {
},
{
...constants.defaultNavigationConfig,
url,
disableThrottling: true,
}
);
Expand All @@ -103,7 +99,7 @@ describe('.prepareNetworkForNavigation()', () => {
});

it('unsets url patterns when empty', async () => {
await prepare.prepareNetworkForNavigation(
await prepare.prepareThrottlingAndNetwork(
sessionMock.asSession(),
{
...constants.defaultSettings,
Expand All @@ -112,7 +108,6 @@ describe('.prepareNetworkForNavigation()', () => {
{
...constants.defaultNavigationConfig,
blockedUrlPatterns: [],
url,
}
);

Expand All @@ -122,7 +117,7 @@ describe('.prepareNetworkForNavigation()', () => {
});

it('blocks url patterns', async () => {
await prepare.prepareNetworkForNavigation(
await prepare.prepareThrottlingAndNetwork(
sessionMock.asSession(),
{
...constants.defaultSettings,
Expand All @@ -131,7 +126,6 @@ describe('.prepareNetworkForNavigation()', () => {
{
...constants.defaultNavigationConfig,
blockedUrlPatterns: ['https://b.example.com'],
url,
}
);

Expand All @@ -141,10 +135,10 @@ describe('.prepareNetworkForNavigation()', () => {
});

it('sets extraHeaders', async () => {
await prepare.prepareNetworkForNavigation(
await prepare.prepareThrottlingAndNetwork(
sessionMock.asSession(),
{...constants.defaultSettings, extraHeaders: {'Cookie': 'monster', 'x-men': 'wolverine'}},
{...constants.defaultNavigationConfig, url}
{...constants.defaultNavigationConfig}
);

expect(sessionMock.sendCommand.findInvocation('Network.setExtraHTTPHeaders')).toEqual({
Expand Down Expand Up @@ -338,3 +332,83 @@ describe('.prepareTargetForNavigationMode()', () => {
});
});
});

describe('.prepareTargetForTimespanMode()', () => {
let driverMock = createMockDriver();

beforeEach(() => {
driverMock = createMockDriver();
sessionMock = driverMock._session;

sessionMock.sendCommand
.mockResponse('Network.enable')
.mockResponse('Network.setUserAgentOverride')
.mockResponse('Emulation.setDeviceMetricsOverride')
.mockResponse('Emulation.setTouchEmulationEnabled')
.mockResponse('Debugger.enable')
.mockResponse('Debugger.setSkipAllPauses')
.mockResponse('Debugger.setAsyncCallStackDepth')
.mockResponse('Network.emulateNetworkConditions')
.mockResponse('Emulation.setCPUThrottlingRate')
.mockResponse('Network.setBlockedURLs')
.mockResponse('Network.setExtraHTTPHeaders');
});

it('emulates the target device', async () => {
await prepare.prepareTargetForTimespanMode(driverMock.asDriver(), {
...constants.defaultSettings,
screenEmulation: {
disabled: false,
mobile: true,
deviceScaleFactor: 2,
width: 200,
height: 300,
},
});

expect(sessionMock.sendCommand.findInvocation('Emulation.setDeviceMetricsOverride')).toEqual({
mobile: true,
deviceScaleFactor: 2,
width: 200,
height: 300,
});
});

it('enables async stacks', async () => {
await prepare.prepareTargetForTimespanMode(driverMock.asDriver(), {
...constants.defaultSettings,
});

const invocations = sessionMock.sendCommand.mock.calls;
const debuggerInvocations = invocations.filter(call => call[0].startsWith('Debugger.'));
expect(debuggerInvocations.map(argList => argList[0])).toEqual([
'Debugger.enable',
'Debugger.setSkipAllPauses',
'Debugger.setAsyncCallStackDepth',
]);
});

it('sets throttling', async () => {
await prepare.prepareTargetForTimespanMode(driverMock.asDriver(), {
...constants.defaultSettings,
throttlingMethod: 'devtools',
});

sessionMock.sendCommand.findInvocation('Network.emulateNetworkConditions');
sessionMock.sendCommand.findInvocation('Emulation.setCPUThrottlingRate');
});

it('sets network environment', async () => {
await prepare.prepareTargetForTimespanMode(driverMock.asDriver(), {
...constants.defaultSettings,
blockedUrlPatterns: ['.jpg'],
extraHeaders: {Cookie: 'name=wolverine'},
});

const blockedInvocation = sessionMock.sendCommand.findInvocation('Network.setBlockedURLs');
expect(blockedInvocation).toEqual({urls: ['.jpg']});

const headersInvocation = sessionMock.sendCommand.findInvocation('Network.setExtraHTTPHeaders');
expect(headersInvocation).toEqual({headers: {Cookie: 'name=wolverine'}});
});
});

0 comments on commit ad3d41c

Please sign in to comment.