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): waitForFCP when tracing #6944

Merged
merged 6 commits into from
Jan 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ test_script:
# FIXME: Exclude Appveyor from running `perf` smoketest until we fix the flake.
# https://github.com/GoogleChrome/lighthouse/issues/5053
# - yarn smoke
- yarn smoke ally pwa pwa2 pwa3 dbw redirects seo offline byte tti
- yarn smoke ally pwa pwa2 pwa3 dbw redirects seo offline byte metrics

cache:
#- chrome-win32 -> appveyor.yml,package.json
Expand Down
17 changes: 17 additions & 0 deletions lighthouse-cli/test/fixtures/delayed-fcp.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<html>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<body>
<!--
This page intentionally contains no content for the first 6 seconds.
After 6 seconds, text is finally added with a setTimeout below.
We are testing Lighthouse's ability to wait for First Contentful Paint, not just page load.
-->
<script>
setTimeout(() => {
patrickhulce marked this conversation as resolved.
Show resolved Hide resolved
const textEl = document.createElement('span');
textEl.textContent = 'Hello, World!';
document.body.appendChild(textEl);
}, 6000);
</script>
</body>
</html>
4 changes: 2 additions & 2 deletions lighthouse-cli/test/smokehouse/run-smoke.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ const SMOKETESTS = [{
config: 'lighthouse-core/config/perf-config.js',
batch: 'perf-metric',
}, {
id: 'tti',
expectations: 'tricky-tti/expectations.js',
id: 'metrics',
expectations: 'tricky-metrics/expectations.js',
config: 'lighthouse-core/config/perf-config.js',
batch: 'parallel-second',
}];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,13 @@ module.exports = [
},
},
},
{
requestedUrl: 'http://localhost:10200/delayed-fcp.html',
finalUrl: 'http://localhost:10200/delayed-fcp.html',
audits: {
'first-contentful-paint': {
rawValue: '>1', // We just want to check that it doesn't error
},
},
},
];
64 changes: 56 additions & 8 deletions lighthouse-core/gather/driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,16 @@ class Driver {
});
}

/**
* Returns a promise that resolves immediately.
* Used for placeholder conditions that we don't want to start waiting for just yet, but still want
* to satisfy the same interface.
* @return {{promise: Promise<void>, cancel: function(): void}}
*/
_waitForNothing() {
return {promise: Promise.resolve(), cancel() {}};
}

/**
* Returns a promise that resolve when a frame has been navigated.
* Used for detecting that our about:blank reset has been completed.
Expand All @@ -522,6 +532,38 @@ class Driver {
});
}

/**
* Returns a promise that resolve when a frame has a FCP.
* @return {{promise: Promise<void>, cancel: function(): void}}
*/
_waitForFCP() {
patrickhulce marked this conversation as resolved.
Show resolved Hide resolved
/** @type {(() => void)} */
let cancel = () => {
throw new Error('_waitForFCP.cancel() called before it was defined');
};

const promise = new Promise(resolve => {
/** @param {LH.Crdp.Page.LifecycleEventEvent} e */
const lifecycleListener = e => {
if (e.name === 'firstContentfulPaint') {
resolve();
this.off('Page.lifecycleEvent', lifecycleListener);
}
};

this.on('Page.lifecycleEvent', lifecycleListener);

cancel = () => {
this.off('Page.lifecycleEvent', lifecycleListener);
};
});

return {
promise,
cancel,
};
}

/**
* Returns a promise that resolves when the network has been idle (after DCL) for
* `networkQuietThresholdMs` ms and a method to cancel internal network listeners/timeout.
Expand Down Expand Up @@ -733,25 +775,28 @@ class Driver {
* @param {number} networkQuietThresholdMs
* @param {number} cpuQuietThresholdMs
* @param {number} maxWaitForLoadedMs
* @param {boolean} shouldWaitForFCP
* @return {Promise<void>}
* @private
*/
async _waitForFullyLoaded(pauseAfterLoadMs, networkQuietThresholdMs, cpuQuietThresholdMs,
maxWaitForLoadedMs) {
maxWaitForLoadedMs, shouldWaitForFCP) {
/** @type {NodeJS.Timer|undefined} */
let maxTimeoutHandle;

// Listener for onload. Resolves on first FCP event.
const waitForFCP = shouldWaitForFCP ? this._waitForFCP() : this._waitForNothing();
// Listener for onload. Resolves pauseAfterLoadMs ms after load.
const waitForLoadEvent = this._waitForLoadEvent(pauseAfterLoadMs);
// Network listener. Resolves when the network has been idle for networkQuietThresholdMs.
const waitForNetworkIdle = this._waitForNetworkIdle(networkQuietThresholdMs);
// CPU listener. Resolves when the CPU has been idle for cpuQuietThresholdMs after network idle.
/** @type {{promise: Promise<void>, cancel: function(): void}|null} */
let waitForCPUIdle = null;
let waitForCPUIdle = this._waitForNothing();

// Wait for both load promises. Resolves on cleanup function the clears load
// timeout timer.
const loadPromise = Promise.all([
waitForFCP.promise,
waitForLoadEvent.promise,
waitForNetworkIdle.promise,
]).then(() => {
Expand All @@ -771,9 +816,10 @@ class Driver {
}).then(_ => {
return async () => {
log.warn('Driver', 'Timed out waiting for page load. Checking if page is hung...');
waitForFCP.cancel();
waitForLoadEvent.cancel();
waitForNetworkIdle.cancel();
waitForCPUIdle && waitForCPUIdle.cancel();
waitForCPUIdle.cancel();

if (await this.isPageHung()) {
log.warn('Driver', 'Page appears to be hung, killing JavaScript...');
Expand Down Expand Up @@ -874,23 +920,25 @@ class Driver {
* possible workaround.
* Resolves on the url of the loaded page, taking into account any redirects.
* @param {string} url
* @param {{waitForLoad?: boolean, waitForNavigated?: boolean, passContext?: LH.Gatherer.PassContext}} options
* @param {{waitForFCP?: boolean, waitForLoad?: boolean, waitForNavigated?: boolean, passContext?: LH.Gatherer.PassContext}} options
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this pulled out as another option for performance reasons, or does fcp not fire in certain passes and we need the option so we don't always wait for the full timeout? It's unfortunate mental overhead since none of the wait* options are independent. Is there a way we can reorganize?

Copy link
Collaborator Author

@patrickhulce patrickhulce Jan 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correcto. On the passes without javascript and offline this can take unnecessarily long when we don't really care about FCP in those passes. In fact, every page that fails the without javascript audit would take 45s+ because all we're doing is checking for the existence of text.

It's unfortunate mental overhead since none of the wait* options are independent. Is there a way we can reorganize?

Agreed. There are really only 4 scenarios despite the 3 total booleans. We could switch to an enum? Opposed to doing it in followup?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are really only 4 scenarios despite the 3 total booleans. We could switch to an enum? Opposed to doing it in followup?

No, that sounds good. Maybe pair it with a switch to "Potential Easy Alternative" (should mostly match what behavior today) or "Real TTI Conditions" (if we're feeling really brave)

* @return {Promise<string>}
*/
async gotoURL(url, options = {}) {
const waitForFCP = options.waitForFCP || false;
const waitForNavigated = options.waitForNavigated || false;
const waitForLoad = options.waitForLoad || false;
const passContext = /** @type {Partial<LH.Gatherer.PassContext>} */ (options.passContext || {});
const disableJS = passContext.disableJavaScript || false;

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

await this._beginNetworkStatusMonitoring(url);
await this._clearIsolatedContextId();

await this.sendCommand('Page.enable');
await this.sendCommand('Page.setLifecycleEventsEnabled', {enabled: true});
await this.sendCommand('Emulation.setScriptExecutionDisabled', {value: disableJS});
// No timeout needed for Page.navigate. See #6413.
const waitforPageNavigateCmd = this._innerSendCommand('Page.navigate', {url});
Expand All @@ -910,7 +958,7 @@ class Driver {
/* eslint-enable max-len */

await this._waitForFullyLoaded(pauseAfterLoadMs, networkQuietThresholdMs, cpuQuietThresholdMs,
maxWaitMs);
maxWaitMs, waitForFCP);
}

// Bring `Page.navigate` errors back into the promise chain. See #6739.
Expand Down
1 change: 1 addition & 0 deletions lighthouse-core/gather/gather-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class GatherRunner {
*/
static async loadPage(driver, passContext) {
const finalUrl = await driver.gotoURL(passContext.url, {
waitForFCP: passContext.passConfig.recordTrace,
waitForLoad: true,
passContext,
});
Expand Down