Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core(driver): extract gotoURL to navigation module #12421

Merged
merged 7 commits into from May 4, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
105 changes: 16 additions & 89 deletions lighthouse-core/gather/driver.js
Expand Up @@ -7,12 +7,11 @@

const Fetcher = require('./fetcher.js');
const ExecutionContext = require('./driver/execution-context.js');
const {waitForFullyLoaded, waitForFrameNavigated} = require('./driver/wait-for-condition.js');

patrickhulce marked this conversation as resolved.
Show resolved Hide resolved
const LHElement = require('../lib/lh-element.js');
const LHError = require('../lib/lh-error.js');
const NetworkRequest = require('../lib/network-request.js');
const EventEmitter = require('events').EventEmitter;
const constants = require('../config/constants.js');

const log = require('lighthouse-logger');
const DevtoolsLog = require('./devtools-log.js');
Expand All @@ -23,17 +22,8 @@ const pageFunctions = require('../lib/page-functions.js');
// Pulled in for Connection type checking.
// eslint-disable-next-line no-unused-vars
const Connection = require('./connections/connection.js');
const NetworkMonitor = require('./driver/network-monitor.js');
const {getBrowserVersion} = require('./driver/environment.js');

// Controls how long to wait after FCP before continuing
const DEFAULT_PAUSE_AFTER_FCP = 0;
// Controls how long to wait after onLoad before continuing
const DEFAULT_PAUSE_AFTER_LOAD = 0;
// Controls how long to wait between network requests before determining the network is quiet
const DEFAULT_NETWORK_QUIET_THRESHOLD = 5000;
// Controls how long to wait between longtasks before determining the CPU is idle, off by default
const DEFAULT_CPU_QUIET_THRESHOLD = 0;
// Controls how long to wait for a response after sending a DevTools protocol command.
const DEFAULT_PROTOCOL_TIMEOUT = 30000;

Expand Down Expand Up @@ -85,12 +75,25 @@ class Driver {
*/
constructor(connection) {
this._connection = connection;
this._networkMonitor = new NetworkMonitor(this);

this.on('Target.attachedToTarget', event => {
this._handleTargetAttached(event).catch(this._handleEventError);
});

this.on('Page.frameNavigated', event => {
// We're only interested in setting autoattach on the root via this method.
// `_handleTargetAttached` takes care of the recursive piece.
if (event.frame.parentId) return;

// Enable auto-attaching to subtargets so we receive iframe information.
this.sendCommand('Target.setAutoAttach', {
flatten: true,
autoAttach: true,
// Pause targets on startup so we don't miss anything
waitForDebuggerOnStart: true,
}).catch(this._handleEventError);
});
adamraine marked this conversation as resolved.
Show resolved Hide resolved

connection.on('protocolevent', this._handleProtocolEvent.bind(this));

/** @private @deprecated Only available for plugin backcompat. */
Expand Down Expand Up @@ -318,6 +321,7 @@ class Driver {
/** @type {NodeJS.Timer|undefined} */
let asyncTimeout;
const timeoutPromise = new Promise((resolve, reject) => {
if (timeout === Infinity) return;
asyncTimeout = setTimeout((() => {
const err = new LHError(
LHError.errors.PROTOCOL_TIMEOUT,
Expand Down Expand Up @@ -376,83 +380,6 @@ class Driver {
return !!this._domainEnabledCounts.get(domain);
}

/**
* Navigate to the given URL. Direct use of this method isn't advised: if
* the current page is already at the given URL, navigation will not occur and
* so the returned promise will only resolve after the MAX_WAIT_FOR_FULLY_LOADED
* timeout. See https://github.com/GoogleChrome/lighthouse/pull/185 for one
* possible workaround.
* Resolves on the url of the loaded page, taking into account any redirects.
* @param {string} url
* @param {{waitForFcp?: boolean, waitForLoad?: boolean, waitForNavigated?: boolean, passContext?: LH.Gatherer.PassContext}} options
* @return {Promise<{finalUrl: string, timedOut: boolean}>}
*/
async gotoURL(url, options = {}) {
const waitForFcp = options.waitForFcp || false;
const waitForNavigated = options.waitForNavigated || false;
const waitForLoad = options.waitForLoad || false;
/** @type {Partial<LH.Gatherer.PassContext>} */
const passContext = options.passContext || {};

if (waitForNavigated && (waitForFcp || waitForLoad)) {
throw new Error('Cannot use both waitForNavigated and another event, pick just one');
}

await this._networkMonitor.enable();
await this.executionContext.clearContextId();

// Enable auto-attaching to subtargets so we receive iframe information
await this.sendCommand('Target.setAutoAttach', {
flatten: true,
autoAttach: true,
// Pause targets on startup so we don't miss anything
waitForDebuggerOnStart: true,
});

await this.sendCommand('Page.enable');
await this.sendCommand('Page.setLifecycleEventsEnabled', {enabled: true});
// No timeout needed for Page.navigate. See https://github.com/GoogleChrome/lighthouse/pull/6413.
const waitforPageNavigateCmd = this._innerSendCommand('Page.navigate', undefined, {url});

let timedOut = false;
if (waitForNavigated) {
await waitForFrameNavigated(this).promise;
} else if (waitForLoad) {
/** @type {Partial<LH.Config.Pass>} */
const passConfig = passContext.passConfig || {};

/* eslint-disable max-len */
let {pauseAfterFcpMs, pauseAfterLoadMs, networkQuietThresholdMs, cpuQuietThresholdMs} = passConfig;
let maxWaitMs = passContext.settings && passContext.settings.maxWaitForLoad;
let maxFCPMs = passContext.settings && passContext.settings.maxWaitForFcp;

if (typeof pauseAfterFcpMs !== 'number') pauseAfterFcpMs = DEFAULT_PAUSE_AFTER_FCP;
if (typeof pauseAfterLoadMs !== 'number') pauseAfterLoadMs = DEFAULT_PAUSE_AFTER_LOAD;
if (typeof networkQuietThresholdMs !== 'number') networkQuietThresholdMs = DEFAULT_NETWORK_QUIET_THRESHOLD;
if (typeof cpuQuietThresholdMs !== 'number') cpuQuietThresholdMs = DEFAULT_CPU_QUIET_THRESHOLD;
if (typeof maxWaitMs !== 'number') maxWaitMs = constants.defaultSettings.maxWaitForLoad;
if (typeof maxFCPMs !== 'number') maxFCPMs = constants.defaultSettings.maxWaitForFcp;
/* eslint-enable max-len */

if (!waitForFcp) maxFCPMs = undefined;
const waitOptions = {pauseAfterFcpMs, pauseAfterLoadMs, networkQuietThresholdMs,
cpuQuietThresholdMs, maxWaitForLoadedMs: maxWaitMs, maxWaitForFcpMs: maxFCPMs};
const loadResult = await waitForFullyLoaded(this, this._networkMonitor, waitOptions);
timedOut = loadResult.timedOut;
}

const finalUrl = await this._networkMonitor.getFinalNavigationUrl() || url;

// Bring `Page.navigate` errors back into the promise chain. See https://github.com/GoogleChrome/lighthouse/pull/6739.
await waitforPageNavigateCmd;
await this._networkMonitor.disable();

return {
finalUrl,
timedOut,
};
}

/**
* @param {string} objectId Object ID for the resolved DOM node
* @param {string} propName Name of the property
Expand Down
109 changes: 109 additions & 0 deletions lighthouse-core/gather/driver/navigation.js
@@ -0,0 +1,109 @@
/**
* @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 NetworkMonitor = require('./network-monitor.js');
const {waitForFullyLoaded, waitForFrameNavigated} = require('./wait-for-condition.js');
const constants = require('../../config/constants.js');

// Controls how long to wait after FCP before continuing
const DEFAULT_PAUSE_AFTER_FCP = 0;
// Controls how long to wait after onLoad before continuing
const DEFAULT_PAUSE_AFTER_LOAD = 0;
// Controls how long to wait between network requests before determining the network is quiet
const DEFAULT_NETWORK_QUIET_THRESHOLD = 5000;
// Controls how long to wait between longtasks before determining the CPU is idle, off by default
const DEFAULT_CPU_QUIET_THRESHOLD = 0;

/** @typedef {{waitUntil: Array<'fcp'|'load'|'navigated'>} & LH.Config.SharedPassNavigationJson & Partial<Pick<LH.Config.Settings, 'maxWaitForFcp'|'maxWaitForLoad'>>} NavigationOptions */

/** @param {NavigationOptions} options */
function resolveWaitForFullyLoadedOptions(options) {
/* eslint-disable max-len */
let {pauseAfterFcpMs, pauseAfterLoadMs, networkQuietThresholdMs, cpuQuietThresholdMs} = options;
let maxWaitMs = options.maxWaitForLoad;
let maxFCPMs = options.maxWaitForFcp;

if (typeof pauseAfterFcpMs !== 'number') pauseAfterFcpMs = DEFAULT_PAUSE_AFTER_FCP;
if (typeof pauseAfterLoadMs !== 'number') pauseAfterLoadMs = DEFAULT_PAUSE_AFTER_LOAD;
if (typeof networkQuietThresholdMs !== 'number') {
networkQuietThresholdMs = DEFAULT_NETWORK_QUIET_THRESHOLD;
}
if (typeof cpuQuietThresholdMs !== 'number') cpuQuietThresholdMs = DEFAULT_CPU_QUIET_THRESHOLD;
if (typeof maxWaitMs !== 'number') maxWaitMs = constants.defaultSettings.maxWaitForLoad;
if (typeof maxFCPMs !== 'number') maxFCPMs = constants.defaultSettings.maxWaitForFcp;
/* eslint-enable max-len */

if (!options.waitUntil.includes('fcp')) maxFCPMs = undefined;

return {
pauseAfterFcpMs,
pauseAfterLoadMs,
networkQuietThresholdMs,
cpuQuietThresholdMs,
maxWaitForLoadedMs: maxWaitMs,
maxWaitForFcpMs: maxFCPMs,
};
}

/**
* Navigates to the given URL, assuming that the page is not already on this URL.
* Resolves on the url of the loaded page, taking into account any redirects.
* Typical use of this method involves navigating to a neutral page such as `about:blank` in between
* navigations.
*
* @param {LH.Gatherer.FRTransitionalDriver} driver
* @param {string} url
* @param {NavigationOptions} options
* @return {Promise<{finalUrl: string, timedOut: boolean}>}
*/
async function gotoURL(driver, url, options) {
const session = driver.defaultSession;
const networkMonitor = new NetworkMonitor(driver.defaultSession);

// Enable the events and network monitor needed to track navigation progress.
await networkMonitor.enable();
await session.sendCommand('Page.enable');
await session.sendCommand('Page.setLifecycleEventsEnabled', {enabled: true});

// No timeout needed for Page.navigate. See https://github.com/GoogleChrome/lighthouse/pull/6413.
patrickhulce marked this conversation as resolved.
Show resolved Hide resolved
session.setNextProtocolTimeout(Infinity);
const waitforPageNavigateCmd = session.sendCommand('Page.navigate', {url});

const waitForNavigated = options.waitUntil.includes('navigated');
const waitForLoad = options.waitUntil.includes('load');
const waitForFcp = options.waitUntil.includes('fcp');

/** @type {Array<Promise<{timedOut: boolean}>>} */
const waitConditionPromises = [];

if (waitForNavigated) {
const navigatedPromise = waitForFrameNavigated(session).promise;
waitConditionPromises.push(navigatedPromise.then(() => ({timedOut: false})));
}

if (waitForLoad) {
const waitOptions = resolveWaitForFullyLoadedOptions(options);
waitConditionPromises.push(waitForFullyLoaded(session, networkMonitor, waitOptions));
} else if (waitForFcp) {
throw new Error('Cannot wait for FCP without waiting for page load');
}

const waitConditions = await Promise.all(waitConditionPromises);
const timedOut = waitConditions.some(condition => condition.timedOut);
const finalUrl = (await networkMonitor.getFinalNavigationUrl()) || url;

// Bring `Page.navigate` errors back into the promise chain. See https://github.com/GoogleChrome/lighthouse/pull/6739.
await waitforPageNavigateCmd;
await networkMonitor.disable();

return {
finalUrl,
timedOut,
};
}

module.exports = {gotoURL};
1 change: 1 addition & 0 deletions lighthouse-core/gather/driver/network-monitor.js
Expand Up @@ -74,6 +74,7 @@ class NetworkMonitor extends EventEmitter {
this._session.on('Page.frameNavigated', this._onFrameNavigated);
this._session.addProtocolMessageListener(this._onProtocolMessage);

await this._session.sendCommand('Page.enable');
await this._session.sendCommand('Network.enable');
}

Expand Down
13 changes: 8 additions & 5 deletions lighthouse-core/gather/gather-runner.js
Expand Up @@ -15,6 +15,7 @@ const i18n = require('../lib/i18n/i18n.js');
const URL = require('../lib/url-shim.js');
const {getBenchmarkIndex} = require('./driver/environment.js');
const storage = require('./driver/storage.js');
const navigation = require('./driver/navigation.js');
const serviceWorkers = require('./driver/service-workers.js');
const WebAppManifest = require('./gatherers/web-app-manifest.js');
const InstallabilityErrors = require('./gatherers/installability-errors.js');
Expand Down Expand Up @@ -79,7 +80,7 @@ class GatherRunner {
static async loadBlank(driver, url = constants.defaultPassConfig.blankPage) {
const status = {msg: 'Resetting state with about:blank', id: 'lh:gather:loadBlank'};
log.time(status);
await driver.gotoURL(url, {waitForNavigated: true});
await navigation.gotoURL(driver, url, {waitUntil: ['navigated']});
log.timeEnd(status);
}

Expand All @@ -100,10 +101,12 @@ class GatherRunner {
};
log.time(status);
try {
const {finalUrl, timedOut} = await driver.gotoURL(passContext.url, {
waitForFcp: passContext.passConfig.recordTrace,
waitForLoad: true,
passContext,
const {finalUrl, timedOut} = await navigation.gotoURL(driver, passContext.url, {
waitUntil: passContext.passConfig.recordTrace ?
['load', 'fcp'] : ['load'],
maxWaitForFcp: passContext.settings.maxWaitForFcp,
maxWaitForLoad: passContext.settings.maxWaitForLoad,
...passContext.passConfig,
});
passContext.url = finalUrl;
if (timedOut) passContext.LighthouseRunWarnings.push(str_(UIStrings.warningTimeout));
Expand Down
2 changes: 2 additions & 0 deletions lighthouse-core/test/fraggle-rock/gather/mock-driver.js
Expand Up @@ -27,6 +27,8 @@ function createMockSession() {
once: createMockOnceFn(),
on: createMockOnFn(),
off: jest.fn(),
addProtocolMessageListener: createMockOnFn(),
removeProtocolMessageListener: jest.fn(),

/** @return {LH.Gatherer.FRProtocolSession} */
asSession() {
Expand Down