-
Notifications
You must be signed in to change notification settings - Fork 9.4k
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: add timeout to all protocol commands #6347
Changes from all commits
fafc315
0e70d6f
368f722
3e9c538
417f0af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,6 +29,8 @@ const DEFAULT_PAUSE_AFTER_LOAD = 0; | |
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 = 5000; | ||
|
||
/** | ||
* @typedef {LH.Protocol.StrictEventEmitter<LH.CrdpEvents>} CrdpEventEmitter | ||
|
@@ -85,6 +87,12 @@ class Driver { | |
* @private | ||
*/ | ||
this._lastSecurityState = null; | ||
|
||
/** | ||
* @type {number} | ||
* @private | ||
*/ | ||
this._nextProtocolTimeout = DEFAULT_PROTOCOL_TIMEOUT; | ||
} | ||
|
||
static get traceCategories() { | ||
|
@@ -240,23 +248,48 @@ class Driver { | |
} | ||
} | ||
|
||
/** | ||
* NOTE: This can eventually be replaced when TypeScript | ||
* resolves https://github.com/Microsoft/TypeScript/issues/5453. | ||
* @param {number} timeout | ||
*/ | ||
setNextProtocolTimeout(timeout) { | ||
this._nextProtocolTimeout = timeout; | ||
} | ||
|
||
/** | ||
* Call protocol methods. | ||
* To configure the timeout for the next call, use 'setNextProtocolTimeout'. | ||
* @template {keyof LH.CrdpCommands} C | ||
* @param {C} method | ||
* @param {LH.CrdpCommands[C]['paramsType']} params, | ||
* @param {LH.CrdpCommands[C]['paramsType']} params | ||
* @return {Promise<LH.CrdpCommands[C]['returnType']>} | ||
*/ | ||
sendCommand(method, ...params) { | ||
const timeout = this._nextProtocolTimeout; | ||
this._nextProtocolTimeout = DEFAULT_PROTOCOL_TIMEOUT; | ||
const domainCommand = /^(\w+)\.(enable|disable)$/.exec(method); | ||
if (domainCommand) { | ||
const enable = domainCommand[2] === 'enable'; | ||
if (!this._shouldToggleDomain(domainCommand[1], enable)) { | ||
return Promise.resolve(); | ||
} | ||
} | ||
|
||
return this._connection.sendCommand(method, ...params); | ||
return new Promise(async (resolve, reject) => { | ||
const asyncTimeout = setTimeout((_ => { | ||
const err = new LHError(LHError.errors.PROTOCOL_TIMEOUT); | ||
err.message += ` Method: ${method}`; | ||
reject(err); | ||
}), timeout); | ||
try { | ||
const result = await this._connection.sendCommand(method, ...params); | ||
clearTimeout(asyncTimeout); | ||
resolve(result); | ||
} catch (err) { | ||
clearTimeout(asyncTimeout); | ||
reject(err); | ||
} | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe drop keep the promise just to the thing that has to be a promise? e.g. return new Promise(async (resolve, reject) => {
const asyncTimeout = setTimeout((_ => {
const err = new LHError(LHError.errors.PROTOCOL_TIMEOUT);
err.message += ` Method: ${method}`;
reject(err);
}), this._protocolTimeout);
try {
const result = await this._connection.sendCommand(method, ...params);
clearTimeout(asyncTimeout);
resolve(result);
} catch (err) {
clearTimeout(asyncTimeout);
reject(err);
}
}); |
||
} | ||
|
||
/** | ||
|
@@ -306,61 +339,46 @@ class Driver { | |
* @param {number|undefined} contextId | ||
* @return {Promise<*>} | ||
*/ | ||
_evaluateInContext(expression, contextId) { | ||
return new Promise((resolve, reject) => { | ||
// If this gets to 60s and it hasn't been resolved, reject the Promise. | ||
const asyncTimeout = setTimeout( | ||
(_ => reject(new Error('The asynchronous expression exceeded the allotted time of 60s'))), | ||
60000 | ||
); | ||
|
||
const evaluationParams = { | ||
// We need to explicitly wrap the raw expression for several purposes: | ||
// 1. Ensure that the expression will be a native Promise and not a polyfill/non-Promise. | ||
// 2. Ensure that errors in the expression are captured by the Promise. | ||
// 3. Ensure that errors captured in the Promise are converted into plain-old JS Objects | ||
// so that they can be serialized properly b/c JSON.stringify(new Error('foo')) === '{}' | ||
expression: `(function wrapInNativePromise() { | ||
const __nativePromise = window.__nativePromise || Promise; | ||
const URL = window.__nativeURL || window.URL; | ||
return new __nativePromise(function (resolve) { | ||
return __nativePromise.resolve() | ||
.then(_ => ${expression}) | ||
.catch(${pageFunctions.wrapRuntimeEvalErrorInBrowserString}) | ||
.then(resolve); | ||
}); | ||
}())`, | ||
includeCommandLineAPI: true, | ||
awaitPromise: true, | ||
returnByValue: true, | ||
contextId, | ||
}; | ||
|
||
this.sendCommand('Runtime.evaluate', evaluationParams).then(response => { | ||
clearTimeout(asyncTimeout); | ||
|
||
if (response.exceptionDetails) { | ||
// An error occurred before we could even create a Promise, should be *very* rare | ||
return reject(new Error(`Evaluation exception: ${response.exceptionDetails.text}`)); | ||
} | ||
|
||
// Protocol should always return a 'result' object, but it is sometimes undefined. See #6026. | ||
if (response.result === undefined) { | ||
return reject(new Error('Runtime.evaluate response did not contain a "result" object')); | ||
} | ||
|
||
const value = response.result.value; | ||
async _evaluateInContext(expression, contextId) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. VSCode suggested this could be async, so I let it :) Thoughts on changing asynchronous syntax? FWIW, V8 just improved the performance of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Totally fine. Since we adopted Node 8 as the base requirement we've mostly just followed the rule of only changing from Promises to async/await when we're touching the code for some other reason (see initial discussion in #3742), but to convert (or not) at will if that's the case. We have some tricky uses of promises (see #6343 :) that need real review, which mass changes make difficult, especially since the indentation level changes and the git diffs end up not always the best, even when ignoring whitespace. We (I?) also end up using git blame a lot, and random changes in unrelated PRs make that annoying :) It's definitely a fuzzy line, though, and sometimes drive by fixes are great. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (This case is totally different, though, because it's fundamentally changing anyways, so it's a prime candidate for fixing.) |
||
const evaluationParams = { | ||
// We need to explicitly wrap the raw expression for several purposes: | ||
// 1. Ensure that the expression will be a native Promise and not a polyfill/non-Promise. | ||
// 2. Ensure that errors in the expression are captured by the Promise. | ||
// 3. Ensure that errors captured in the Promise are converted into plain-old JS Objects | ||
// so that they can be serialized properly b/c JSON.stringify(new Error('foo')) === '{}' | ||
expression: `(function wrapInNativePromise() { | ||
const __nativePromise = window.__nativePromise || Promise; | ||
const URL = window.__nativeURL || window.URL; | ||
return new __nativePromise(function (resolve) { | ||
return __nativePromise.resolve() | ||
.then(_ => ${expression}) | ||
.catch(${pageFunctions.wrapRuntimeEvalErrorInBrowserString}) | ||
.then(resolve); | ||
}); | ||
}())`, | ||
includeCommandLineAPI: true, | ||
awaitPromise: true, | ||
returnByValue: true, | ||
contextId, | ||
}; | ||
|
||
if (value && value.__failedInBrowser) { | ||
return reject(Object.assign(new Error(), value)); | ||
} else { | ||
resolve(value); | ||
} | ||
}).catch(err => { | ||
clearTimeout(asyncTimeout); | ||
reject(err); | ||
}); | ||
}); | ||
this.setNextProtocolTimeout(60000); | ||
const response = await this.sendCommand('Runtime.evaluate', evaluationParams); | ||
connorjclark marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (response.exceptionDetails) { | ||
// An error occurred before we could even create a Promise, should be *very* rare | ||
return Promise.reject(new Error(`Evaluation exception: ${response.exceptionDetails.text}`)); | ||
} | ||
// Protocol should always return a 'result' object, but it is sometimes undefined. See #6026. | ||
if (response.result === undefined) { | ||
return Promise.reject( | ||
new Error('Runtime.evaluate response did not contain a "result" object')); | ||
} | ||
const value = response.result.value; | ||
if (value && value.__failedInBrowser) { | ||
return Promise.reject(Object.assign(new Error(), value)); | ||
} else { | ||
return value; | ||
} | ||
} | ||
|
||
/** | ||
|
@@ -863,24 +881,14 @@ class Driver { | |
* @param {number} [timeout] | ||
* @return {Promise<string>} | ||
*/ | ||
getRequestContent(requestId, timeout = 1000) { | ||
async getRequestContent(requestId, timeout = 1000) { | ||
requestId = NetworkRequest.getRequestIdForBackend(requestId); | ||
|
||
return new Promise((resolve, reject) => { | ||
// If this takes more than 1s, reject the Promise. | ||
// Why? Encoding issues can lead to hanging getResponseBody calls: https://github.com/GoogleChrome/lighthouse/pull/4718 | ||
const err = new LHError(LHError.errors.REQUEST_CONTENT_TIMEOUT); | ||
const asyncTimeout = setTimeout((_ => reject(err)), timeout); | ||
|
||
this.sendCommand('Network.getResponseBody', {requestId}).then(result => { | ||
clearTimeout(asyncTimeout); | ||
// Ignoring result.base64Encoded, which indicates if body is already encoded | ||
resolve(result.body); | ||
}).catch(e => { | ||
clearTimeout(asyncTimeout); | ||
reject(e); | ||
}); | ||
}); | ||
// Encoding issues may lead to hanging getResponseBody calls: https://github.com/GoogleChrome/lighthouse/pull/4718 | ||
// driver.sendCommand will handle timeout after 1s. | ||
this.setNextProtocolTimeout(timeout); | ||
const result = await this.sendCommand('Network.getResponseBody', {requestId}); | ||
return result.body; | ||
} | ||
|
||
async listenForSecurityStateChanges() { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -169,17 +169,20 @@ const ERRORS = { | |
lhrRuntimeError: true, | ||
}, | ||
|
||
// Protocol timeout failures | ||
REQUEST_CONTENT_TIMEOUT: { | ||
code: 'REQUEST_CONTENT_TIMEOUT', | ||
message: strings.requestContentTimeout, | ||
}, | ||
|
||
// URL parsing failures | ||
INVALID_URL: { | ||
code: 'INVALID_URL', | ||
message: strings.urlInvalid, | ||
}, | ||
|
||
// Protocol timeout failures | ||
PROTOCOL_TIMEOUT: { | ||
code: 'PROTOCOL_TIMEOUT', | ||
message: strings.protocolTimeout, | ||
connorjclark marked this conversation as resolved.
Show resolved
Hide resolved
|
||
lhrRuntimeError: true, | ||
}, | ||
|
||
// Hey! When adding a new error type, update lighthouse-result.proto too. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❤️ |
||
}; | ||
|
||
/** @type {Record<keyof typeof ERRORS, LighthouseErrorDefinition>} */ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this still true now? Maybe add a bit more to the comment then because I haven't the faintest clue how we can remove the method and the variadic types header at the top of that issue just makes me dizzy 🤣
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, this is referring to
sendCommand
. That might be the confusion...My understanding is that the linked issue will eventually allow types such as:
If so, we'd be able to type that variadic parameter as
LH.CrdpCommands[C]['paramsType'] + [?{timeout: number}]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oooooh, that makes much more sense. Move the comments down there then near that parameter? :)