[go: nahoru, domu]

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(gather-runner): add PageLoadError base artifact #9236

Merged
merged 4 commits into from
Jun 21, 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: 0 additions & 2 deletions lighthouse-cli/test/smokehouse/error-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ module.exports = {
maxWaitForLoad: 5000,
onlyAudits: [
'first-contentful-paint',
// TODO: this audit collects a non-base artifact, allowing the runtimeError to be collected.
'content-width',
],
},
};
32 changes: 26 additions & 6 deletions lighthouse-cli/test/smokehouse/error-expectations.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,44 @@ module.exports = [
lhr: {
requestedUrl: 'http://localhost:10200/infinite-loop.html',
finalUrl: 'http://localhost:10200/infinite-loop.html',
audits: {},
// TODO: runtimeError only exists because of selection of audits.
runtimeError: {code: 'PAGE_HUNG'},
audits: {
'first-contentful-paint': {
scoreDisplayMode: 'error',
errorMessage: 'Required traces gatherer did not run.',
},
},
},
artifacts: {
ViewportDimensions: {code: 'PAGE_HUNG'},
PageLoadError: {code: 'PAGE_HUNG'},
devtoolsLogs: {
'pageLoadError-defaultPass': [/* ... */],
},
traces: {
'pageLoadError-defaultPass': {traceEvents: [/* ... */]},
},
},
},
{
lhr: {
requestedUrl: 'https://expired.badssl.com',
finalUrl: 'https://expired.badssl.com/',
audits: {},
// TODO: runtimeError only exists because of selection of audits.
runtimeError: {code: 'INSECURE_DOCUMENT_REQUEST'},
audits: {
'first-contentful-paint': {
scoreDisplayMode: 'error',
errorMessage: 'Required traces gatherer did not run.',
},
},
},
artifacts: {
ViewportDimensions: {code: 'INSECURE_DOCUMENT_REQUEST'},
PageLoadError: {code: 'INSECURE_DOCUMENT_REQUEST'},
devtoolsLogs: {
'pageLoadError-defaultPass': [/* ... */],
},
traces: {
'pageLoadError-defaultPass': {traceEvents: [/* ... */]},
},
},
},
];
36 changes: 10 additions & 26 deletions lighthouse-core/gather/gather-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -408,28 +408,6 @@ class GatherRunner {
log.timeEnd(apStatus);
}

/**
* Generate a set of artfiacts for the given pass as if all the gatherers
Copy link
Collaborator

Choose a reason for hiding this comment

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

aw, no more artfiacts 😢

* failed with the given pageLoadError.
* @param {LH.Gatherer.PassContext} passContext
* @param {LHError} pageLoadError
* @return {{pageLoadError: LHError, artifacts: Partial<LH.GathererArtifacts>}}
*/
static generatePageLoadErrorArtifacts(passContext, pageLoadError) {
/** @type {Partial<Record<keyof LH.GathererArtifacts, LHError>>} */
const errorArtifacts = {};
for (const gathererDefn of passContext.passConfig.gatherers) {
const gatherer = gathererDefn.instance;
errorArtifacts[gatherer.name] = pageLoadError;
}

return {
pageLoadError,
// @ts-ignore - TODO(bckenny): figure out how to usefully type errored artifacts.
artifacts: errorArtifacts,
};
}

/**
* Takes the results of each gatherer phase for each gatherer and uses the
* last produced value (that's not undefined) as the artifact for that
Expand Down Expand Up @@ -495,6 +473,7 @@ class GatherRunner {
settings: options.settings,
URL: {requestedUrl: options.requestedUrl, finalUrl: options.requestedUrl},
Timing: [],
PageLoadError: null,
};
}

Expand Down Expand Up @@ -594,7 +573,10 @@ class GatherRunner {
Object.assign(artifacts, passResults.artifacts);

// If we encountered a pageLoadError, don't try to keep loading the page in future passes.
if (passResults.pageLoadError) break;
if (passResults.pageLoadError) {
baseArtifacts.PageLoadError = passResults.pageLoadError;
break;
}

if (isFirstPass) {
await GatherRunner.populateBaseArtifacts(passContext);
Expand All @@ -606,8 +588,9 @@ class GatherRunner {
GatherRunner.finalizeBaseArtifacts(baseArtifacts);
return /** @type {LH.Artifacts} */ ({...baseArtifacts, ...artifacts}); // Cast to drop Partial<>.
} catch (err) {
// cleanup on error
// Clean up on error. Don't await so that the root error, not a disposal error, is shown.
GatherRunner.disposeDriver(driver, options);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I like the simplification, but now we can't await it in the success case :/

maybe it doesn't matter and we can add a comment calling out why it doesn't matter?

worth noting that it does clearDataForOrigin which given the recent changes in Chrome has taken multiple seconds on my machine lately :(

Copy link
Member Author

Choose a reason for hiding this comment

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

but now we can't await it in the success case

Good catch, that wasn't intentional. I wonder how bad it would be to await the error case... Presumably it was so errors in disposing didn't overwrite the originating error, which probably often go hand in hand?

Kind of ridiculous we can't express what we mean here.

I could also just change it back, it's not a huge win.

Copy link
Collaborator

Choose a reason for hiding this comment

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

yeah maybe just change it back with a comment so we don't forget this again next time :)

Copy link
Member Author

Choose a reason for hiding this comment

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

This pattern originally popped up way back in #639 (in the course of @wardpeet adding the multiple tabs check!), though originally we didn't wait for disconnect in the failure or success cases.

Looking at #2359, where the successful path was integrated into the promise chain, I'm half convinced we should be doing await GatherRunner.disposeDriver(driver, options).catch(() => {}); in the error case to avoid ever exiting before Chrome is able to exit, but I'm not able to induce a problem, so leaving it as it was :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think I'm with you on .catch even if we don't await it, but w/e :)


throw err;
}
}
Expand Down Expand Up @@ -660,14 +643,15 @@ class GatherRunner {
// Disable throttling so the afterPass analysis isn't throttled
await driver.setThrottling(passContext.settings, {useThrottling: false});

// If there were any load errors, treat all gatherers as if they errored.
// In case of load error, save log and trace with an error prefix, return no artifacts for this pass.
const pageLoadError = GatherRunner.getPageLoadError(passContext, loadData, possibleNavError);
if (pageLoadError) {
log.error('GatherRunner', pageLoadError.friendlyMessage, passContext.url);
passContext.LighthouseRunWarnings.push(pageLoadError.friendlyMessage);
GatherRunner._addLoadDataToBaseArtifacts(passContext, loadData,
`pageLoadError-${passConfig.passName}`);
return GatherRunner.generatePageLoadErrorArtifacts(passContext, pageLoadError);

return {artifacts: {}, pageLoadError};
}

// If no error, save devtoolsLog and trace.
Expand Down
10 changes: 8 additions & 2 deletions lighthouse-core/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -352,12 +352,18 @@ class Runner {
}

/**
* Returns first runtimeError found in artifacts.
* Searches a pass's artifacts for any `lhrRuntimeError` error artifacts.
* Returns the first one found or `null` if none found.
* @param {LH.Artifacts} artifacts
* @return {LH.Result['runtimeError']|undefined}
*/
static getArtifactRuntimeError(artifacts) {
for (const possibleErrorArtifact of Object.values(artifacts)) {
const possibleErrorArtifacts = [
artifacts.PageLoadError, // Preferentially use `PageLoadError`, if it exists.
...Object.values(artifacts), // Otherwise check amongst all artifacts.
];

for (const possibleErrorArtifact of possibleErrorArtifacts) {
if (possibleErrorArtifact instanceof LHError && possibleErrorArtifact.lhrRuntimeError) {
const errorMessage = possibleErrorArtifact.friendlyMessage || possibleErrorArtifact.message;

Expand Down
33 changes: 20 additions & 13 deletions lighthouse-core/test/gather/gather-runner-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -424,9 +424,10 @@ describe('GatherRunner', function() {
assert.equal(tests.calledCleanBrowserCaches, true);
});

it('fails artifacts with network errors', async () => {
it('returns a pageLoadError and no artifacts with network errors', async () => {
const requestedUrl = 'https://example.com';
// This page load error should be overriden by NO_DOCUMENT_REQUEST for being more specific
// This page load error should be overriden by NO_DOCUMENT_REQUEST (for being
// more specific) since requestedUrl does not match any network request in fixture.
const navigationError = new LHError(LHError.errors.NO_FCP);
const driver = Object.assign({}, fakeDriver, {
online: true,
Expand All @@ -450,13 +451,14 @@ describe('GatherRunner', function() {
baseArtifacts: await GatherRunner.initializeBaseArtifacts({driver, settings, requestedUrl}),
};

const {artifacts} = await GatherRunner.runPass(passContext);
const {artifacts, pageLoadError} = await GatherRunner.runPass(passContext);
expect(passContext.LighthouseRunWarnings).toHaveLength(1);
expect(artifacts.TestGatherer).toBeInstanceOf(Error);
expect(artifacts.TestGatherer.code).toEqual('NO_DOCUMENT_REQUEST');
expect(pageLoadError).toBeInstanceOf(Error);
expect(pageLoadError.code).toEqual('NO_DOCUMENT_REQUEST');
expect(artifacts).toEqual({});
});

it('fails artifacts with navigation errors', async () => {
it('returns a pageLoadError and no artifacts with navigation errors', async () => {
const requestedUrl = 'https://example.com';
// This time, NO_FCP should win because it's the only error left.
const navigationError = new LHError(LHError.errors.NO_FCP);
Expand Down Expand Up @@ -485,10 +487,11 @@ describe('GatherRunner', function() {
baseArtifacts: await GatherRunner.initializeBaseArtifacts({driver, settings, requestedUrl}),
};

const {artifacts} = await GatherRunner.runPass(passContext);
const {artifacts, pageLoadError} = await GatherRunner.runPass(passContext);
expect(passContext.LighthouseRunWarnings).toHaveLength(1);
expect(artifacts.TestGatherer).toBeInstanceOf(Error);
expect(artifacts.TestGatherer.code).toEqual('NO_FCP');
expect(pageLoadError).toBeInstanceOf(Error);
expect(pageLoadError.code).toEqual('NO_FCP');
expect(artifacts).toEqual({});
});

it('does not clear origin storage with flag --disable-storage-reset', () => {
Expand Down Expand Up @@ -806,7 +809,8 @@ describe('GatherRunner', function() {
const options = {driver, requestedUrl, settings: config.settings};
const artifacts = await GatherRunner.run(config.passes, options);

expect(artifacts.TestGatherer.code).toEqual('NO_DOCUMENT_REQUEST');
expect(artifacts.PageLoadError.code).toEqual('NO_DOCUMENT_REQUEST');
expect(artifacts.TestGatherer).toBeUndefined();

// The only loadData available should be prefixed with `pageLoadError-`.
expect(Object.keys(artifacts.traces)).toEqual(['pageLoadError-firstPass']);
Expand Down Expand Up @@ -856,12 +860,15 @@ describe('GatherRunner', function() {
expect(t2.called).toBe(true);
expect(t3.called).toBe(false);

// But only t1 has a valid artifact, t2 has an error artifact, and t3 never ran.
// But only t1 has a valid artifact; t2 and t3 aren't defined.
expect(artifacts.Test1).toBe('MyArtifact');
expect(artifacts.Test2).toBeInstanceOf(LHError);
expect(artifacts.Test2.code).toEqual('NO_FCP');
expect(artifacts.Test2).toBeUndefined();
expect(artifacts.Test3).toBeUndefined();

// PageLoadError artifact has the error.
expect(artifacts.PageLoadError).toBeInstanceOf(LHError);
expect(artifacts.PageLoadError.code).toEqual('NO_FCP');

// firstPass has a saved trace and devtoolsLog, secondPass has an error trace and log.
expect(Object.keys(artifacts.traces)).toEqual(['firstPass', 'pageLoadError-secondPass']);
expect(Object.keys(artifacts.devtoolsLogs)).toEqual(['firstPass', 'pageLoadError-secondPass']);
Expand Down
65 changes: 53 additions & 12 deletions lighthouse-core/test/runner-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -631,13 +631,18 @@ describe('Runner', () => {
});
});

it('includes a top-level runtimeError when a gatherer throws one', async () => {
describe('lhr.runtimeError', () => {
const NO_FCP = LHError.errors.NO_FCP;
class RuntimeErrorGatherer extends Gatherer {
afterPass() {
throw new LHError(NO_FCP);
}
}
class RuntimeError2Gatherer extends Gatherer {
afterPass() {
throw new LHError(LHError.errors.NO_SCREENSHOTS);
}
}
class WarningAudit extends Audit {
static get meta() {
return {
Expand All @@ -652,19 +657,55 @@ describe('Runner', () => {
}
}

const config = new Config({
passes: [{gatherers: [RuntimeErrorGatherer]}],
const configJson = {
passes: [
{gatherers: [RuntimeErrorGatherer]},
{gatherers: [RuntimeError2Gatherer], passName: 'second'},
],
audits: [WarningAudit],
});
const {lhr} = await Runner.run(null, {url: 'https://example.com/', config, driverMock});
};

// Audit error included the runtimeError
assert.strictEqual(lhr.audits['test-audit'].scoreDisplayMode, 'error');
assert.ok(lhr.audits['test-audit'].errorMessage.includes(NO_FCP.code));
// And it bubbled up to the runtimeError.
assert.strictEqual(lhr.runtimeError.code, NO_FCP.code);
expect(lhr.runtimeError.message)
.toBeDisplayString(/Something .*\(NO_FCP\)/);
it('includes a top-level runtimeError when a gatherer throws one', async () => {
const config = new Config(configJson);
const {lhr} = await Runner.run(null, {url: 'https://example.com/', config, driverMock});

// Audit error included the runtimeError
expect(lhr.audits['test-audit'].scoreDisplayMode).toEqual('error');
expect(lhr.audits['test-audit'].errorMessage).toEqual(expect.stringContaining(NO_FCP.code));

// And it bubbled up to the runtimeError.
expect(lhr.runtimeError.code).toEqual(NO_FCP.code);
expect(lhr.runtimeError.message).toBeDisplayString(/Something .*\(NO_FCP\)/);
Copy link
Member Author

Choose a reason for hiding this comment

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

these are unchanged, just nested now to share the test gatherer/audit/config with the following test (and assert -> expect)

});

it('includes a pageLoadError runtimeError over any gatherer runtimeErrors', async () => {
const url = 'https://www.reddit.com/r/nba';
let firstLoad = true;
Copy link
Collaborator

Choose a reason for hiding this comment

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

firstLoad strikes again 😢

Copy link
Member Author

Choose a reason for hiding this comment

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

firstLoad strikes again 😢

yeah, I'm sorry!

const errorDriverMock = Object.assign({}, driverMock, {
online: true,
// Loads the page successfully in the first pass, fails with PAGE_HUNG in the second.
async gotoURL(url) {
if (url.includes('blank')) return null;
if (firstLoad) {
firstLoad = false;
return url;
} else {
throw new LHError(LHError.errors.PAGE_HUNG);
}
},
});

const config = new Config(configJson);
const {lhr} = await Runner.run(null, {url, config, driverMock: errorDriverMock});

// Audit error still includes the gatherer runtimeError.
expect(lhr.audits['test-audit'].scoreDisplayMode).toEqual('error');
expect(lhr.audits['test-audit'].errorMessage).toEqual(expect.stringContaining(NO_FCP.code));

// But top-level runtimeError is the pageLoadError.
expect(lhr.runtimeError.code).toEqual(LHError.errors.PAGE_HUNG.code);
expect(lhr.runtimeError.message).toBeDisplayString(/because the page stopped responding/);
});
});

it('localized errors thrown from driver', async () => {
Expand Down
2 changes: 2 additions & 0 deletions types/artifacts.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ declare global {
URL: {requestedUrl: string, finalUrl: string};
/** The timing instrumentation of the gather portion of a run. */
Timing: Artifacts.MeasureEntry[];
/** If loading the page failed, value is the error that caused it. Otherwise null. */
PageLoadError: LighthouseError | null;
}

/**
Expand Down