Skip to content

Commit

Permalink
Merge 63a25b9 into ac85955
Browse files Browse the repository at this point in the history
  • Loading branch information
adamraine committed Nov 25, 2020
2 parents ac85955 + 63a25b9 commit bf0dff3
Show file tree
Hide file tree
Showing 13 changed files with 581 additions and 61 deletions.
@@ -0,0 +1,43 @@
/**
* @license Copyright 2020 The Lighthouse Authors. 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';

/**
* @fileoverview Computed Largest Contentful Paint (LCP) for all frames.
*/

const makeComputedArtifact = require('../computed-artifact.js');
const ComputedMetric = require('./metric.js');
const LHError = require('../../lib/lh-error.js');

class LargestContentfulPaintAllFrames extends ComputedMetric {
/**
* TODO: Simulate LCP all frames in lantern.
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static async computeSimulatedMetric() {
throw new Error('LCP All Frames not implemented in lantern');
}

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

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

module.exports = makeComputedArtifact(LargestContentfulPaintAllFrames);
6 changes: 6 additions & 0 deletions lighthouse-core/computed/metrics/timing-summary.js
Expand Up @@ -10,6 +10,7 @@ const Speedline = require('../speedline.js');
const FirstContentfulPaint = require('./first-contentful-paint.js');
const FirstMeaningfulPaint = require('./first-meaningful-paint.js');
const LargestContentfulPaint = require('./largest-contentful-paint.js');
const LargestContentfulPaintAllFrames = require('./largest-contentful-paint-all-frames.js');
const FirstCPUIdle = require('./first-cpu-idle.js');
const Interactive = require('./interactive.js');
const CumulativeLayoutShift = require('./cumulative-layout-shift.js');
Expand Down Expand Up @@ -44,6 +45,7 @@ class TimingSummary {
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 largestContentfulPaintAllFrames = await requestOrUndefined(LargestContentfulPaintAllFrames, metricComputationData); // eslint-disable-line max-len
const firstCPUIdle = await requestOrUndefined(FirstCPUIdle, metricComputationData);
const interactive = await requestOrUndefined(Interactive, metricComputationData);
const cumulativeLayoutShift = await requestOrUndefined(CumulativeLayoutShift, trace);
Expand All @@ -65,6 +67,8 @@ class TimingSummary {
firstMeaningfulPaintTs: firstMeaningfulPaint.timestamp,
largestContentfulPaint: largestContentfulPaint && largestContentfulPaint.timing,
largestContentfulPaintTs: largestContentfulPaint && largestContentfulPaint.timestamp,
largestContentfulPaintAllFrames: largestContentfulPaintAllFrames && largestContentfulPaintAllFrames.timing, // eslint-disable-line max-len
largestContentfulPaintAllFramesTs: largestContentfulPaintAllFrames && largestContentfulPaintAllFrames.timestamp, // eslint-disable-line max-len
firstCPUIdle: firstCPUIdle && firstCPUIdle.timing,
firstCPUIdleTs: firstCPUIdle && firstCPUIdle.timestamp,
interactive: interactive && interactive.timing,
Expand Down Expand Up @@ -92,6 +96,8 @@ class TimingSummary {
observedFirstMeaningfulPaintTs: traceOfTab.timestamps.firstMeaningfulPaint,
observedLargestContentfulPaint: traceOfTab.timings.largestContentfulPaint,
observedLargestContentfulPaintTs: traceOfTab.timestamps.largestContentfulPaint,
observedLargestContentfulPaintAllFrames: traceOfTab.timings.largestContentfulPaintAllFrames,
observedLargestContentfulPaintAllFramesTs: traceOfTab.timestamps.largestContentfulPaintAllFrames, // eslint-disable-line max-len
observedTraceEnd: traceOfTab.timings.traceEnd,
observedTraceEndTs: traceOfTab.timestamps.traceEnd,
observedLoad: traceOfTab.timings.load,
Expand Down
4 changes: 4 additions & 0 deletions lighthouse-core/lib/lh-error.js
Expand Up @@ -276,6 +276,10 @@ const ERRORS = {
code: 'NO_LCP',
message: UIStrings.badTraceRecording,
},
NO_LCP_ALL_FRAMES: {
code: 'NO_LCP_ALL_FRAMES',
message: UIStrings.badTraceRecording,
},
UNSUPPORTED_OLD_CHROME: {
code: 'UNSUPPORTED_OLD_CHROME',
message: UIStrings.oldChromeDoesNotSupportFeature,
Expand Down
76 changes: 55 additions & 21 deletions lighthouse-core/lib/tracehouse/trace-processor.js
Expand Up @@ -477,6 +477,39 @@ class TraceProcessor {
evt.name === SCHEDULABLE_TASK_TITLE_ALT3;
}

/**
* @param {{candidateEventName: string, invalidateEventName: string, events: LH.TraceEvent[], timeOriginEvt: LH.TraceEvent}} options
* @return {{lcp: LH.TraceEvent | undefined, invalidated: boolean}}
*/
static computeValidLCP(options) {
const {
candidateEventName,
invalidateEventName,
events,
timeOriginEvt,
} = options;

let lcp;
let invalidated = false;
// Iterate the events backwards.
for (let i = events.length - 1; i >= 0; i--) {
const e = events[i];
// If the event's timestamp is before the time origin, stop.
if (e.ts <= timeOriginEvt.ts) break;
// If the last lcp event in the trace is 'Invalidate', there is inconclusive data to determine LCP.
if (e.name === invalidateEventName) {
invalidated = true;
break;
}
// If not an lcp 'Candidate', keep iterating.
if (e.name !== candidateEventName) continue;
// Found the last LCP candidate in the trace, let's use it.
lcp = e;
break;
}

return {lcp, invalidated};
}

/**
* Finds key trace events, identifies main process/thread, and returns timings of trace events
Expand Down Expand Up @@ -512,6 +545,14 @@ class TraceProcessor {
// Compute the key frame timings for the main frame.
const frameTimings = this.computeKeyTimingsForFrame(frameEvents, {timeOriginEvt});

// Compute LCP for all frames.
const lcpAllFramesEvt = this.computeValidLCP({
candidateEventName: 'NavStartToLargestContentfulPaint::Candidate::AllFrames::UKM',
invalidateEventName: 'NavStartToLargestContentfulPaint::Invalidate::AllFrames::UKM',
events: keyEvents,
timeOriginEvt,
}).lcp;

// Subset all trace events to just our tab's process (incl threads other than main)
// stable-sort events to keep them correctly nested.
const processEvents = TraceProcessor
Expand All @@ -530,6 +571,8 @@ class TraceProcessor {
// Ensure our traceEnd reflects all page activity.
const traceEnd = this.computeTraceEnd(trace.traceEvents, timeOriginEvt);

/** @param {number|undefined} ts */
const maybeGetTiming = (ts) => ts === undefined ? undefined : (ts - timeOriginEvt.ts) / 1000;
// This could be much more concise with object spread, but the consensus is that explicitness is
// preferred over brevity here.
return {
Expand All @@ -543,6 +586,7 @@ class TraceProcessor {
firstContentfulPaint: frameTimings.timings.firstContentfulPaint,
firstMeaningfulPaint: frameTimings.timings.firstMeaningfulPaint,
largestContentfulPaint: frameTimings.timings.largestContentfulPaint,
largestContentfulPaintAllFrames: maybeGetTiming(lcpAllFramesEvt && lcpAllFramesEvt.ts),
traceEnd: traceEnd.timing,
load: frameTimings.timings.load,
domContentLoaded: frameTimings.timings.domContentLoaded,
Expand All @@ -553,6 +597,7 @@ class TraceProcessor {
firstContentfulPaint: frameTimings.timestamps.firstContentfulPaint,
firstMeaningfulPaint: frameTimings.timestamps.firstMeaningfulPaint,
largestContentfulPaint: frameTimings.timestamps.largestContentfulPaint,
largestContentfulPaintAllFrames: lcpAllFramesEvt && lcpAllFramesEvt.ts,
traceEnd: traceEnd.timestamp,
load: frameTimings.timestamps.load,
domContentLoaded: frameTimings.timestamps.domContentLoaded,
Expand All @@ -562,6 +607,7 @@ class TraceProcessor {
firstContentfulPaintEvt: frameTimings.firstContentfulPaintEvt,
firstMeaningfulPaintEvt: frameTimings.firstMeaningfulPaintEvt,
largestContentfulPaintEvt: frameTimings.largestContentfulPaintEvt,
largestContentfulPaintAllFramesEvt: lcpAllFramesEvt,
loadEvt: frameTimings.loadEvt,
domContentLoadedEvt: frameTimings.domContentLoadedEvt,
fmpFellBack: frameTimings.fmpFellBack,
Expand Down Expand Up @@ -670,24 +716,12 @@ class TraceProcessor {
// 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 time origin, stop.
if (e.ts <= timeOriginEvt.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 lcpResult = this.computeValidLCP({
candidateEventName: 'largestContentfulPaint::Candidate',
invalidateEventName: 'largestContentfulPaint::Invalidate',
events: frameEvents,
timeOriginEvt,
});

const load = frameEvents.find(e => e.name === 'loadEventEnd' && e.ts > timeOriginEvt.ts);
const domContentLoaded = frameEvents.find(
Expand All @@ -702,7 +736,7 @@ class TraceProcessor {
firstPaint: getTimestamp(firstPaint),
firstContentfulPaint: getTimestamp(firstContentfulPaint),
firstMeaningfulPaint: getTimestamp(firstMeaningfulPaint),
largestContentfulPaint: getTimestamp(largestContentfulPaint),
largestContentfulPaint: getTimestamp(lcpResult.lcp),
load: getTimestamp(load),
domContentLoaded: getTimestamp(domContentLoaded),
};
Expand All @@ -727,11 +761,11 @@ class TraceProcessor {
firstPaintEvt: firstPaint,
firstContentfulPaintEvt: firstContentfulPaint,
firstMeaningfulPaintEvt: firstMeaningfulPaint,
largestContentfulPaintEvt: largestContentfulPaint,
largestContentfulPaintEvt: lcpResult.lcp,
loadEvt: load,
domContentLoadedEvt: domContentLoaded,
fmpFellBack,
lcpInvalidated,
lcpInvalidated: lcpResult.invalidated,
};
}
}
Expand Down
63 changes: 63 additions & 0 deletions lighthouse-core/test/audits/__snapshots__/metrics-test.js.snap
@@ -1,5 +1,56 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Performance: metrics evaluates valid input (with lcp from all frames) correctly 1`] = `
Object {
"cumulativeLayoutShift": 0,
"estimatedInputLatency": 16,
"estimatedInputLatencyTs": undefined,
"firstCPUIdle": 668,
"firstCPUIdleTs": 10238937930,
"firstContentfulPaint": 668,
"firstContentfulPaintTs": 10238937930,
"firstMeaningfulPaint": 668,
"firstMeaningfulPaintTs": 10238937930,
"interactive": 668,
"interactiveTs": 10238937930,
"largestContentfulPaint": 668,
"largestContentfulPaintAllFrames": 2258,
"largestContentfulPaintAllFramesTs": 10240528038,
"largestContentfulPaintTs": 10238937930,
"maxPotentialFID": 16,
"observedCumulativeLayoutShift": 0,
"observedDomContentLoaded": 605,
"observedDomContentLoadedTs": 10238874657,
"observedFirstContentfulPaint": 668,
"observedFirstContentfulPaintTs": 10238937930,
"observedFirstMeaningfulPaint": 668,
"observedFirstMeaningfulPaintTs": 10238937930,
"observedFirstPaint": 668,
"observedFirstPaintTs": 10238937930,
"observedFirstVisualChange": 671,
"observedFirstVisualChangeTs": 10238940760,
"observedLargestContentfulPaint": 668,
"observedLargestContentfulPaintAllFrames": 2258,
"observedLargestContentfulPaintAllFramesTs": 10240528038,
"observedLargestContentfulPaintTs": 10238937930,
"observedLastVisualChange": 704,
"observedLastVisualChangeTs": 10238973760,
"observedLoad": 684,
"observedLoadTs": 10238953674,
"observedNavigationStart": 0,
"observedNavigationStartTs": 10238269760,
"observedSpeedIndex": 688,
"observedSpeedIndexTs": 10238957680,
"observedTimeOrigin": 0,
"observedTimeOriginTs": 10238269760,
"observedTraceEnd": 5994,
"observedTraceEndTs": 10244264001,
"speedIndex": 688,
"speedIndexTs": 10238957760,
"totalBlockingTime": 0,
}
`;

exports[`Performance: metrics evaluates valid input (with lcp) correctly 1`] = `
Object {
"cumulativeLayoutShift": 0,
Expand All @@ -14,6 +65,8 @@ Object {
"interactive": 4462,
"interactiveTs": undefined,
"largestContentfulPaint": 2758,
"largestContentfulPaintAllFrames": undefined,
"largestContentfulPaintAllFramesTs": undefined,
"largestContentfulPaintTs": undefined,
"maxPotentialFID": 1336,
"observedCumulativeLayoutShift": 0,
Expand All @@ -28,6 +81,8 @@ Object {
"observedFirstVisualChange": 1105,
"observedFirstVisualChangeTs": 713038128064,
"observedLargestContentfulPaint": 1122,
"observedLargestContentfulPaintAllFrames": undefined,
"observedLargestContentfulPaintAllFramesTs": undefined,
"observedLargestContentfulPaintTs": 713038144775,
"observedLastVisualChange": 1722,
"observedLastVisualChangeTs": 713038745064,
Expand Down Expand Up @@ -61,6 +116,8 @@ Object {
"interactive": 1582,
"interactiveTs": 225415754204,
"largestContentfulPaint": undefined,
"largestContentfulPaintAllFrames": undefined,
"largestContentfulPaintAllFramesTs": undefined,
"largestContentfulPaintTs": undefined,
"maxPotentialFID": 198,
"observedCumulativeLayoutShift": 0,
Expand All @@ -75,6 +132,8 @@ Object {
"observedFirstVisualChange": 520,
"observedFirstVisualChangeTs": 225414692015,
"observedLargestContentfulPaint": undefined,
"observedLargestContentfulPaintAllFrames": undefined,
"observedLargestContentfulPaintAllFramesTs": undefined,
"observedLargestContentfulPaintTs": undefined,
"observedLastVisualChange": 818,
"observedLastVisualChangeTs": 225414990015,
Expand Down Expand Up @@ -108,6 +167,8 @@ Object {
"interactive": 3427,
"interactiveTs": undefined,
"largestContentfulPaint": undefined,
"largestContentfulPaintAllFrames": undefined,
"largestContentfulPaintAllFramesTs": undefined,
"largestContentfulPaintTs": undefined,
"maxPotentialFID": 396,
"observedCumulativeLayoutShift": 0,
Expand All @@ -122,6 +183,8 @@ Object {
"observedFirstVisualChange": 520,
"observedFirstVisualChangeTs": 225414692015,
"observedLargestContentfulPaint": undefined,
"observedLargestContentfulPaintAllFrames": undefined,
"observedLargestContentfulPaintAllFramesTs": undefined,
"observedLargestContentfulPaintTs": undefined,
"observedLastVisualChange": 818,
"observedLastVisualChangeTs": 225414990015,
Expand Down
18 changes: 18 additions & 0 deletions lighthouse-core/test/audits/metrics-test.js
Expand Up @@ -14,6 +14,9 @@ const pwaDevtoolsLog = require('../fixtures/traces/progressive-app-m60.devtools.
const lcpTrace = require('../fixtures/traces/lcp-m78.json');
const lcpDevtoolsLog = require('../fixtures/traces/lcp-m78.devtools.log.json');

const lcpAllFramesTrace = require('../fixtures/traces/lcp-all-frames-m89.json');
const lcpAllFramesDevtoolsLog = require('../fixtures/traces/lcp-all-frames-m89.devtools.log.json');

const artifactsTrace = require('../results/artifacts/defaultPass.trace.json');
const artifactsDevtoolsLog = require('../results/artifacts/defaultPass.devtoolslog.json');

Expand Down Expand Up @@ -65,6 +68,21 @@ describe('Performance: metrics', () => {
expect(result.details.items[0]).toMatchSnapshot();
});

it('evaluates valid input (with lcp from all frames) correctly', async () => {
const artifacts = {
traces: {
[MetricsAudit.DEFAULT_PASS]: lcpAllFramesTrace,
},
devtoolsLogs: {
[MetricsAudit.DEFAULT_PASS]: lcpAllFramesDevtoolsLog,
},
};

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

it('evaluates valid input (with CLS) correctly', async () => {
const artifacts = {
traces: {
Expand Down

0 comments on commit bf0dff3

Please sign in to comment.