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): move evaluateOnNewDocument to executionContext #12381

Merged
merged 10 commits into from
Apr 21, 2021
32 changes: 4 additions & 28 deletions lighthouse-core/gather/driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -398,17 +398,6 @@ class Driver {
return !!this._domainEnabledCounts.get(domain);
}

/**
* Add a script to run at load time of all future page loads.
* @param {string} scriptSource
* @return {Promise<LH.Crdp.Page.AddScriptToEvaluateOnLoadResponse>} Identifier of the added script.
*/
evaluateScriptOnNewDocument(scriptSource) {
return this.sendCommand('Page.addScriptToEvaluateOnLoad', {
scriptSource,
});
}

/**
* @return {Promise<LH.Crdp.ServiceWorker.WorkerVersionUpdatedEvent>}
*/
Expand Down Expand Up @@ -885,20 +874,6 @@ class Driver {
}
}

/**
* Cache native functions/objects inside window
* so we are sure polyfills do not overwrite the native implementations
* @return {Promise<void>}
*/
async cacheNatives() {
await this.evaluateScriptOnNewDocument(`
window.__nativePromise = Promise;
window.__nativeURL = URL;
window.__ElementMatches = Element.prototype.matches;
window.__perfNow = performance.now.bind(performance);
`);
}

/**
* Use a RequestIdleCallback shim for tests run with simulated throttling, so that the deadline can be used without
* a penalty
Expand All @@ -907,9 +882,10 @@ class Driver {
*/
async registerRequestIdleCallbackWrap(settings) {
if (settings.throttlingMethod === 'simulate') {
const scriptStr = `(${pageFunctions.wrapRequestIdleCallbackString})
(${settings.throttling.cpuSlowdownMultiplier})`;
await this.evaluateScriptOnNewDocument(scriptStr);
await this.executionContext.evaluateOnNewDocument(
pageFunctions.wrapRequestIdleCallback,
{args: [settings.throttling.cpuSlowdownMultiplier]}
);
}
}

Expand Down
76 changes: 71 additions & 5 deletions lighthouse-core/gather/driver/execution-context.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
*/
'use strict';

/* global window */

const pageFunctions = require('../../lib/page-functions.js');

class ExecutionContext {
Expand Down Expand Up @@ -83,11 +85,10 @@ class ExecutionContext {
// 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 = globalThis.__nativePromise || Promise;
const URL = globalThis.__nativeURL || globalThis.URL;
${ExecutionContext._cachedNativesPreamble};
Copy link
Collaborator

Choose a reason for hiding this comment

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

very nice!

globalThis.__lighthouseExecutionContextId = ${contextId};
return new __nativePromise(function (resolve) {
return __nativePromise.resolve()
return new Promise(function (resolve) {
return Promise.resolve()
.then(_ => ${expression})
.catch(${pageFunctions.wrapRuntimeEvalErrorInBrowserString})
.then(resolve);
Expand Down Expand Up @@ -164,14 +165,79 @@ class ExecutionContext {
* @return {FlattenedPromise<R>}
*/
evaluate(mainFn, options) {
const argsSerialized = options.args.map(arg => JSON.stringify(arg)).join(',');
const argsSerialized = ExecutionContext.serializeArguments(options.args);
const depsSerialized = options.deps ? options.deps.join('\n') : '';
const expression = `(() => {
${depsSerialized}
return (${mainFn})(${argsSerialized});
})()`;
return this.evaluateAsync(expression, options);
}

/**
* Evaluate a function on every new frame from now on.
* @template {any[]} T
patrickhulce marked this conversation as resolved.
Show resolved Hide resolved
* @param {((...args: T) => void)} mainFn The main function to call.
* @param {{args: T, deps?: Array<Function|string>}} options `args` should
Copy link
Collaborator

Choose a reason for hiding this comment

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

yayyy

* match the args of `mainFn`, and can be any serializable value. `deps` are functions that must be
* defined for `mainFn` to work.
* @return {Promise<void>}
*/
async evaluateOnNewDocument(mainFn, options) {
const argsSerialized = ExecutionContext.serializeArguments(options.args);
const depsSerialized = options.deps ? options.deps.join('\n') : '';

const expression = `
${ExecutionContext._cachedNativesPreamble};
${depsSerialized};
(${mainFn})(${argsSerialized});
`;

await this._session.sendCommand('Page.addScriptToEvaluateOnNewDocument', {source: expression});
}

/**
* Cache native functions/objects inside window so we are sure polyfills do not overwrite the
* native implementations when the page loads.
* @return {Promise<void>}
*/
async cacheNativesOnNewDocument() {
await this.evaluateOnNewDocument(() => {
window.__nativePromise = window.Promise;
window.__nativeURL = window.URL;
window.__nativePerformance = window.performance;
window.__ElementMatches = window.Element.prototype.matches;
// Ensure the native `performance.now` is not overwritable.
const performance = window.performance;
const performanceNow = window.performance.now;
Object.defineProperty(performance, 'now', {
value: () => performanceNow.call(performance),
writable: false,
});
}, {args: []});
}

/**
* Prefix every script evaluation with a shadowing of common globals that tend to be ponyfilled
* incorrectly by many sites. This allows functions to still refer to `Promise` instead of
* Lighthouse-specific backups like `__nativePromise`.
patrickhulce marked this conversation as resolved.
Show resolved Hide resolved
*/
static get _cachedNativesPreamble() {
return [
'const Promise = globalThis.__nativePromise || globalThis.Promise',
'const URL = globalThis.__nativeURL || globalThis.URL',
'const performance = globalThis.__nativePerformance || globalThis.performance',
].join(';\n');
}
patrickhulce marked this conversation as resolved.
Show resolved Hide resolved

/**
* Serializes an array of arguments for use in an `eval` string across the protocol.
* @param {any[]} args
patrickhulce marked this conversation as resolved.
Show resolved Hide resolved
* @return {string}
*/
static serializeArguments(args) {
return args.map(arg => arg === undefined ? 'undefined' : JSON.stringify(arg)).join(',');
}
}

module.exports = ExecutionContext;
8 changes: 4 additions & 4 deletions lighthouse-core/gather/driver/wait-for-condition.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/
'use strict';

/* global window */
/* global window, performance */

const log = require('lighthouse-logger');
const LHError = require('../../lib/lh-error.js');
Expand Down Expand Up @@ -274,7 +274,7 @@ function registerPerformanceObserverInPage() {
// Do not re-register if we've already run this script.
if (window.____lastLongTask !== undefined) return;

window.____lastLongTask = window.__perfNow();
window.____lastLongTask = performance.now();
const observer = new window.PerformanceObserver(entryList => {
const entries = entryList.getEntries();
for (const entry of entries) {
Expand Down Expand Up @@ -303,8 +303,8 @@ function checkTimeSinceLastLongTaskInPage() {
// at some point farish (several hundred ms) into the future and the time at which it executes isn't
// a reliable indicator of long task existence, instead we check if any information has changed.
// See https://developer.chrome.com/blog/timer-throttling-in-chrome-88/
return new window.__nativePromise(resolve => {
const firstAttemptTs = window.__perfNow();
return new Promise(resolve => {
const firstAttemptTs = performance.now();
const firstAttemptLastLongTaskTs = window.____lastLongTask || 0;

setTimeout(() => {
Expand Down
2 changes: 1 addition & 1 deletion lighthouse-core/gather/gather-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ class GatherRunner {
await driver.beginEmulation(options.settings);
await driver.enableRuntimeEvents();
await driver.enableAsyncStacks();
await driver.cacheNatives();
await driver.executionContext.cacheNativesOnNewDocument();
await driver.dismissJavaScriptDialogs();
await driver.registerRequestIdleCallbackWrap(options.settings);
if (resetStorage) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

const Gatherer = require('../gatherer.js');

/* global document, window, HTMLLinkElement, SVGScriptElement */
/* global document, window, performance, HTMLLinkElement, SVGScriptElement */

/** @typedef {{href: string, media: string, msSinceHTMLEnd: number, matches: boolean}} MediaChange */
/** @typedef {{tagName: 'LINK', url: string, href: string, rel: string, media: string, disabled: boolean, mediaChanges: Array<MediaChange>}} LinkTag */
Expand All @@ -37,7 +37,7 @@ function installMediaListener() {
const mediaChange = {
href: this.href,
media: val,
msSinceHTMLEnd: Date.now() - window.performance.timing.responseEnd,
msSinceHTMLEnd: Date.now() - performance.timing.responseEnd,
matches: window.matchMedia(val).matches,
};
// @ts-expect-error - `___linkMediaChanges` created above.
Expand Down Expand Up @@ -203,8 +203,9 @@ class TagsBlockingFirstPaint extends Gatherer {
* @param {LH.Gatherer.PassContext} passContext
*/
async beforePass(passContext) {
// Don't return return value of `evaluateScriptOnNewDocument`.
await passContext.driver.evaluateScriptOnNewDocument(`(${installMediaListener.toString()})()`);
const {executionContext} = passContext.driver;
// Don't return return value of `evaluateOnNewDocument`.
await executionContext.evaluateOnNewDocument(installMediaListener, {args: []});
}

/**
Expand Down
2 changes: 1 addition & 1 deletion lighthouse-core/lib/page-functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,6 @@ module.exports = {
getNodeLabel: getNodeLabel,
getNodeLabelString: getNodeLabel.toString(),
isPositionFixedString: isPositionFixed.toString(),
wrapRequestIdleCallbackString: wrapRequestIdleCallback.toString(),
wrapRequestIdleCallback,
getBoundingClientRectString: getBoundingClientRect.toString(),
};
18 changes: 14 additions & 4 deletions lighthouse-core/test/gather/driver/execution-context-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,11 +206,12 @@ describe('.evaluate', () => {
const {expression} = mockFn.findInvocation('Runtime.evaluate');
const expected = `
(function wrapInNativePromise() {
const __nativePromise = globalThis.__nativePromise || Promise;
const URL = globalThis.__nativeURL || globalThis.URL;
const Promise = globalThis.__nativePromise || globalThis.Promise;
const URL = globalThis.__nativeURL || globalThis.URL;
const performance = globalThis.__nativePerformance || globalThis.performance;
globalThis.__lighthouseExecutionContextId = undefined;
return new __nativePromise(function (resolve) {
return __nativePromise.resolve()
return new Promise(function (resolve) {
return Promise.resolve()
.then(_ => (() => {

return (function main(value) {
Expand Down Expand Up @@ -329,3 +330,12 @@ function square(val) {
expect(eval(code)).toEqual({a: 5, b: 100, passThru: 'hello'});
});
});

describe('.serializeArguments', () => {
it('should serialize a list of differently typed arguments', () => {
const args = [undefined, 1, 'foo', null, {x: {y: {z: [2]}}}];
expect(ExecutionContext.serializeArguments(args)).toEqual(
`undefined,1,"foo",null,{"x":{"y":{"z":[2]}}}`
);
});
});
9 changes: 3 additions & 6 deletions lighthouse-core/test/gather/fake-driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,24 +57,21 @@ function makeFakeDriver({protocolGetVersionResponse}) {
enableAsyncStacks() {
return Promise.resolve();
},
evaluateScriptOnLoad() {
return Promise.resolve();
},
cleanBrowserCaches() {},
clearDataForOrigin() {},
getImportantStorageWarning() {
return Promise.resolve(undefined);
},
cacheNatives() {
return Promise.resolve();
},
executionContext: {
evaluateAsync() {
return Promise.resolve({});
},
evaluate() {
return Promise.resolve({});
},
cacheNativesOnNewDocument() {
return Promise.resolve();
},
},
/** @param {{x: number, y: number}} position */
scrollTo(position) {
Expand Down
8 changes: 3 additions & 5 deletions lighthouse-core/test/gather/gather-runner-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,6 @@ class EmulationDriver extends Driver {
assertNoSameOriginServiceWorkerClients() {
return Promise.resolve();
}
cacheNatives() {
return Promise.resolve();
}
registerPerformanceObserver() {
return Promise.resolve();
}
Expand Down Expand Up @@ -125,6 +122,7 @@ function resetDefaultMockResponses() {
.mockResponse('Network.setBlockedURLs')
.mockResponse('Network.setExtraHTTPHeaders')
.mockResponse('Network.setUserAgentOverride')
.mockResponse('Page.addScriptToEvaluateOnNewDocument')
.mockResponse('Page.enable')
.mockResponse('ServiceWorker.enable');
}
Expand Down Expand Up @@ -339,7 +337,7 @@ describe('GatherRunner', function() {
dismissJavaScriptDialogs: asyncFunc,
enableRuntimeEvents: asyncFunc,
enableAsyncStacks: asyncFunc,
cacheNatives: asyncFunc,
executionContext: {cacheNativesOnNewDocument: asyncFunc},
gotoURL: asyncFunc,
registerRequestIdleCallbackWrap: asyncFunc,
cleanBrowserCaches: createCheck('calledCleanBrowserCaches'),
Expand Down Expand Up @@ -530,7 +528,7 @@ describe('GatherRunner', function() {
dismissJavaScriptDialogs: asyncFunc,
enableRuntimeEvents: asyncFunc,
enableAsyncStacks: asyncFunc,
cacheNatives: asyncFunc,
executionContext: {cacheNativesOnNewDocument: asyncFunc},
gotoURL: asyncFunc,
registerPerformanceObserver: asyncFunc,
registerRequestIdleCallbackWrap: asyncFunc,
Expand Down
6 changes: 3 additions & 3 deletions types/externs.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,11 +393,11 @@ declare global {

interface Window {
// Cached native functions/objects for use in case the page overwrites them.
// See: `driver.cacheNatives`.
// See: `executionContext.cacheNativesOnNewDocument`.
__nativePromise: PromiseConstructor;
__nativeURL: URL;
__nativePerformance: Performance;
__nativeURL: typeof URL;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this was typed incorrectly, but we never use it :)

__ElementMatches: Element['matches'];
__perfNow: Performance['now'];

/** Used for monitoring long tasks in the test page. */
____lastLongTask?: number;
Expand Down