Skip to content

Commit

Permalink
core: add largest-contentful-paint metric to JSON (#9706)
Browse files Browse the repository at this point in the history
  • Loading branch information
connorjclark authored and brendankenny committed Oct 2, 2019
1 parent 9b41045 commit ac67b37
Show file tree
Hide file tree
Showing 16 changed files with 3,999 additions and 4 deletions.
13 changes: 11 additions & 2 deletions lighthouse-core/audits/metrics.js
Expand Up @@ -10,6 +10,7 @@ const TraceOfTab = require('../computed/trace-of-tab.js');
const Speedline = require('../computed/speedline.js');
const FirstContentfulPaint = require('../computed/metrics/first-contentful-paint.js');
const FirstMeaningfulPaint = require('../computed/metrics/first-meaningful-paint.js');
const LargestContentfulPaint = require('../computed/metrics/largest-contentful-paint.js');
const FirstCPUIdle = require('../computed/metrics/first-cpu-idle.js');
const Interactive = require('../computed/metrics/interactive.js');
const SpeedIndex = require('../computed/metrics/speed-index.js');
Expand Down Expand Up @@ -40,7 +41,6 @@ class Metrics extends Audit {
const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
const metricComputationData = {trace, devtoolsLog, settings: context.settings};


/**
* @template TArtifacts
* @template TReturn
Expand All @@ -56,6 +56,7 @@ class Metrics extends Audit {
const speedline = await Speedline.request(trace, context);
const firstContentfulPaint = await FirstContentfulPaint.request(metricComputationData, context);
const firstMeaningfulPaint = await FirstMeaningfulPaint.request(metricComputationData, context);
const largestContentfulPaint = await requestOrUndefined(LargestContentfulPaint, metricComputationData); // eslint-disable-line max-len
const firstCPUIdle = await requestOrUndefined(FirstCPUIdle, metricComputationData);
const interactive = await requestOrUndefined(Interactive, metricComputationData);
const speedIndex = await requestOrUndefined(SpeedIndex, metricComputationData);
Expand All @@ -69,6 +70,8 @@ class Metrics extends Audit {
firstContentfulPaintTs: firstContentfulPaint.timestamp,
firstMeaningfulPaint: firstMeaningfulPaint.timing,
firstMeaningfulPaintTs: firstMeaningfulPaint.timestamp,
largestContentfulPaint: largestContentfulPaint && largestContentfulPaint.timing,
largestContentfulPaintTs: largestContentfulPaint && largestContentfulPaint.timestamp,
firstCPUIdle: firstCPUIdle && firstCPUIdle.timing,
firstCPUIdleTs: firstCPUIdle && firstCPUIdle.timestamp,
interactive: interactive && interactive.timing,
Expand All @@ -88,6 +91,8 @@ class Metrics extends Audit {
observedFirstContentfulPaintTs: traceOfTab.timestamps.firstContentfulPaint,
observedFirstMeaningfulPaint: traceOfTab.timings.firstMeaningfulPaint,
observedFirstMeaningfulPaintTs: traceOfTab.timestamps.firstMeaningfulPaint,
observedLargestContentfulPaint: traceOfTab.timings.largestContentfulPaint,
observedLargestContentfulPaintTs: traceOfTab.timestamps.largestContentfulPaint,
observedTraceEnd: traceOfTab.timings.traceEnd,
observedTraceEndTs: traceOfTab.timestamps.traceEnd,
observedLoad: traceOfTab.timings.load,
Expand Down Expand Up @@ -115,7 +120,7 @@ class Metrics extends Audit {
const details = {
type: 'debugdata',
// TODO: Consider not nesting metrics under `items`.
items: [metrics],
items: [metrics, {lcpInvalidated: traceOfTab.lcpInvalidated}],
};

return {
Expand All @@ -132,6 +137,8 @@ class Metrics extends Audit {
* @property {number=} firstContentfulPaintTs
* @property {number} firstMeaningfulPaint
* @property {number=} firstMeaningfulPaintTs
* @property {number=} largestContentfulPaint
* @property {number=} largestContentfulPaintTs
* @property {number=} firstCPUIdle
* @property {number=} firstCPUIdleTs
* @property {number=} interactive
Expand All @@ -149,6 +156,8 @@ class Metrics extends Audit {
* @property {number} observedFirstContentfulPaintTs
* @property {number=} observedFirstMeaningfulPaint
* @property {number=} observedFirstMeaningfulPaintTs
* @property {number=} observedLargestContentfulPaint
* @property {number=} observedLargestContentfulPaintTs
* @property {number=} observedTraceEnd
* @property {number=} observedTraceEndTs
* @property {number=} observedLoad
Expand Down
Expand Up @@ -38,7 +38,7 @@ class LanternFirstMeaningfulPaint extends LanternMetric {
return LanternFirstContentfulPaint.getFirstPaintBasedGraph(
dependencyGraph,
fmp,
// See LanterFirstContentfulPaint's getOptimisticGraph implementation for a longer description
// See LanternFirstContentfulPaint's getOptimisticGraph implementation for a longer description
// of why we exclude script initiated resources here.
node => node.hasRenderBlockingPriority() && node.initiatorType !== 'script'
);
Expand Down
41 changes: 41 additions & 0 deletions lighthouse-core/computed/metrics/largest-contentful-paint.js
@@ -0,0 +1,41 @@
/**
* @license Copyright 2019 Google Inc. 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 makeComputedArtifact = require('../computed-artifact.js');
const ComputedMetric = require('./metric.js');
const LHError = require('../../lib/lh-error.js');

class LargestContentfulPaint extends ComputedMetric {
/**
* @param {LH.Artifacts.MetricComputationData} data
* @param {LH.Audit.Context} context
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
// eslint-disable-next-line no-unused-vars
static computeSimulatedMetric(data, context) {
throw new Error('Unimplemented');
}

/**
* @param {LH.Artifacts.MetricComputationData} data
* @return {Promise<LH.Artifacts.Metric>}
*/
static async computeObservedMetric(data) {
const {traceOfTab} = data;
if (!traceOfTab.timestamps.largestContentfulPaint) {
throw new LHError(LHError.errors.NO_LCP);
}

return {
// LCP established as existing, so cast
timing: /** @type {number} */ (traceOfTab.timings.largestContentfulPaint),
timestamp: traceOfTab.timestamps.largestContentfulPaint,
};
}
}

module.exports = makeComputedArtifact(LargestContentfulPaint);
4 changes: 4 additions & 0 deletions lighthouse-core/lib/lh-error.js
Expand Up @@ -253,6 +253,10 @@ const ERRORS = {
code: 'NO_FMP',
message: UIStrings.badTraceRecording,
},
NO_LCP: {
code: 'NO_LCP',
message: UIStrings.badTraceRecording,
},

// TTI calculation failures
FMP_TOO_LATE_FOR_FCPUI: {code: 'FMP_TOO_LATE_FOR_FCPUI', message: UIStrings.pageLoadTookTooLong},
Expand Down
2 changes: 2 additions & 0 deletions lighthouse-core/lib/minify-trace.js
Expand Up @@ -64,6 +64,8 @@ const traceEventsToKeepInProcess = new Set([
'firstMeaningfulPaintCandidate',
'loadEventEnd',
'domContentLoadedEventEnd',
'largestContentfulPaint::Invalidate',
'largestContentfulPaint::Candidate',
]);

/**
Expand Down
26 changes: 26 additions & 0 deletions lighthouse-core/lib/tracehouse/trace-processor.js
Expand Up @@ -524,6 +524,28 @@ class TraceProcessor {
firstMeaningfulPaint = lastCandidate;
}

// LCP comes from the latest `largestContentfulPaint::Candidate`, but it can be invalidated
// by a `largestContentfulPaint::Invalidate` event. In the case that the last candidate is
// invalidated, the value will be undefined.
let largestContentfulPaint;
let lcpInvalidated = false;
// Iterate the events backwards.
for (let i = frameEvents.length - 1; i >= 0; i--) {
const e = frameEvents[i];
// If the event's timestamp is before the navigation start, stop.
if (e.ts <= navigationStart.ts) break;
// If the last lcp event in the trace is 'Invalidate', there is inconclusive data to determine LCP.
if (e.name === 'largestContentfulPaint::Invalidate') {
lcpInvalidated = true;
break;
}
// If not an lcp 'Candidate', keep iterating.
if (e.name !== 'largestContentfulPaint::Candidate') continue;
// Found the last LCP candidate in the trace, let's use it.
largestContentfulPaint = e;
break;
}

const load = frameEvents.find(e => e.name === 'loadEventEnd' && e.ts > navigationStart.ts);
const domContentLoaded = frameEvents.find(
e => e.name === 'domContentLoadedEventEnd' && e.ts > navigationStart.ts
Expand Down Expand Up @@ -551,6 +573,7 @@ class TraceProcessor {
firstPaint: getTimestamp(firstPaint),
firstContentfulPaint: getTimestamp(firstContentfulPaint),
firstMeaningfulPaint: getTimestamp(firstMeaningfulPaint),
largestContentfulPaint: getTimestamp(largestContentfulPaint),
traceEnd: fakeEndOfTraceEvt.ts,
load: getTimestamp(load),
domContentLoaded: getTimestamp(domContentLoaded),
Expand All @@ -566,6 +589,7 @@ class TraceProcessor {
firstPaint: maybeGetTiming(timestamps.firstPaint),
firstContentfulPaint: maybeGetTiming(timestamps.firstContentfulPaint),
firstMeaningfulPaint: maybeGetTiming(timestamps.firstMeaningfulPaint),
largestContentfulPaint: maybeGetTiming(timestamps.largestContentfulPaint),
traceEnd: getTiming(timestamps.traceEnd),
load: maybeGetTiming(timestamps.load),
domContentLoaded: maybeGetTiming(timestamps.domContentLoaded),
Expand All @@ -581,9 +605,11 @@ class TraceProcessor {
firstPaintEvt: firstPaint,
firstContentfulPaintEvt: firstContentfulPaint,
firstMeaningfulPaintEvt: firstMeaningfulPaint,
largestContentfulPaintEvt: largestContentfulPaint,
loadEvt: load,
domContentLoadedEvt: domContentLoaded,
fmpFellBack,
lcpInvalidated,
};
}
}
Expand Down
46 changes: 46 additions & 0 deletions lighthouse-core/test/audits/__snapshots__/metrics-test.js.snap
@@ -1,5 +1,47 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Performance: metrics evaluates valid input (with lcp) correctly 1`] = `
Object {
"estimatedInputLatency": 1324,
"estimatedInputLatencyTs": undefined,
"firstCPUIdle": 14907,
"firstCPUIdleTs": 1671236823075,
"firstContentfulPaint": 4702,
"firstContentfulPaintTs": 1671226617803,
"firstMeaningfulPaint": 4702,
"firstMeaningfulPaintTs": 1671226617803,
"interactive": 14907,
"interactiveTs": 1671236823075,
"largestContentfulPaint": 15024,
"largestContentfulPaintTs": 1671236939268,
"observedDomContentLoaded": 14907,
"observedDomContentLoadedTs": 1671236823075,
"observedFirstContentfulPaint": 4702,
"observedFirstContentfulPaintTs": 1671226617803,
"observedFirstMeaningfulPaint": 4702,
"observedFirstMeaningfulPaintTs": 1671226617803,
"observedFirstPaint": 4702,
"observedFirstPaintTs": 1671226617803,
"observedFirstVisualChange": 4685,
"observedFirstVisualChangeTs": 1671226600754,
"observedLargestContentfulPaint": 15024,
"observedLargestContentfulPaintTs": 1671236939268,
"observedLastVisualChange": 4685,
"observedLastVisualChangeTs": 1671226600754,
"observedLoad": 40464,
"observedLoadTs": 1671262379989,
"observedNavigationStart": 0,
"observedNavigationStartTs": 1671221915754,
"observedSpeedIndex": 5715,
"observedSpeedIndexTs": 1671227631023,
"observedTraceEnd": 44549,
"observedTraceEndTs": 1671266464483,
"speedIndex": 5715,
"speedIndexTs": 1671227630754,
"totalBlockingTime": 1935,
}
`;

exports[`Performance: metrics evaluates valid input correctly 1`] = `
Object {
"estimatedInputLatency": 78,
Expand All @@ -12,6 +54,8 @@ Object {
"firstMeaningfulPaintTs": undefined,
"interactive": 3427,
"interactiveTs": undefined,
"largestContentfulPaint": undefined,
"largestContentfulPaintTs": undefined,
"observedDomContentLoaded": 560,
"observedDomContentLoadedTs": 225414732309,
"observedFirstContentfulPaint": 499,
Expand All @@ -22,6 +66,8 @@ Object {
"observedFirstPaintTs": 225414670868,
"observedFirstVisualChange": 520,
"observedFirstVisualChangeTs": 225414692015,
"observedLargestContentfulPaint": undefined,
"observedLargestContentfulPaintTs": undefined,
"observedLastVisualChange": 818,
"observedLastVisualChangeTs": 225414990015,
"observedLoad": 2199,
Expand Down
20 changes: 19 additions & 1 deletion lighthouse-core/test/audits/metrics-test.js
Expand Up @@ -11,6 +11,9 @@ const TTIComputed = require('../../computed/metrics/interactive.js');
const pwaTrace = require('../fixtures/traces/progressive-app-m60.json');
const pwaDevtoolsLog = require('../fixtures/traces/progressive-app-m60.devtools.log.json');

const lcpTrace = require('../fixtures/traces/lcp-m79.json');
const lcpDevtoolsLog = require('../fixtures/traces/lcp-m79.devtools.log.json');

/* eslint-env jest */

describe('Performance: metrics', () => {
Expand All @@ -29,7 +32,22 @@ describe('Performance: metrics', () => {
expect(result.details.items[0]).toMatchSnapshot();
});

it('does to fail the entire audit when TTI errors', async () => {
it('evaluates valid input (with lcp) correctly', async () => {
const artifacts = {
traces: {
[MetricsAudit.DEFAULT_PASS]: lcpTrace,
},
devtoolsLogs: {
[MetricsAudit.DEFAULT_PASS]: lcpDevtoolsLog,
},
};

const context = {settings: {throttlingMethod: 'devtools'}, computedCache: new Map()};
const result = await MetricsAudit.audit(artifacts, context);
expect(result.details.items[0]).toMatchSnapshot();
});

it('does not fail the entire audit when TTI errors', async () => {
const artifacts = {
traces: {
[MetricsAudit.DEFAULT_PASS]: pwaTrace,
Expand Down
@@ -0,0 +1,42 @@
/**
* @license Copyright 2019 Google Inc. 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 assert = require('assert');

const LargestContentfulPaint = require('../../../computed/metrics/largest-contentful-paint.js'); // eslint-disable-line max-len
const trace = require('../../fixtures/traces/lcp-m79.json');
const devtoolsLog = require('../../fixtures/traces/lcp-m79.devtools.log.json');
const invalidTrace = require('../../fixtures/traces/progressive-app-m60.json');
const invalidDevtoolsLog = require('../../fixtures/traces/progressive-app-m60.devtools.log.json');

/* eslint-env jest */

describe('Metrics: LCP', () => {
it('should error when computing a simulated value', async () => {
const settings = {throttlingMethod: 'simulate'};
const context = {settings, computedCache: new Map()};
const resultPromise = LargestContentfulPaint.request({trace, devtoolsLog, settings}, context);
await expect(resultPromise).rejects.toThrow();
});

it('should compute an observed value', async () => {
const settings = {throttlingMethod: 'provided'};
const context = {settings, computedCache: new Map()};
const result = await LargestContentfulPaint.request({trace, devtoolsLog, settings}, context);

assert.equal(Math.round(result.timing), 15024);
assert.equal(result.timestamp, 1671236939268);
});

it('should fail to compute an observed value for old trace', async () => {
const settings = {throttlingMethod: 'provided'};
const context = {settings, computedCache: new Map()};
const resultPromise = LargestContentfulPaint.request(
{trace: invalidTrace, devtoolsLog: invalidDevtoolsLog, settings}, context);
await expect(resultPromise).rejects.toThrow('NO_LCP');
});
});
1 change: 1 addition & 0 deletions lighthouse-core/test/computed/trace-of-tab-test.js
Expand Up @@ -75,6 +75,7 @@ describe('TraceOfTabComputed', () => {
tts: 866553,
},
fmpFellBack: false,
lcpInvalidated: false,
loadEvt: {
args: {
frame: '0x25a638821e30',
Expand Down

Large diffs are not rendered by default.

3,716 changes: 3,716 additions & 0 deletions lighthouse-core/test/fixtures/traces/lcp-m79.json

Large diffs are not rendered by default.

0 comments on commit ac67b37

Please sign in to comment.