Skip to content
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
4 changes: 2 additions & 2 deletions cli/test/smokehouse/test-definitions/dobetterweb.js
Original file line number Diff line number Diff line change
Expand Up @@ -515,8 +515,8 @@ const expectations = {
_excludeRunner: 'devtools',
details: {items: {0: {
timeToFirstByte: '450+/-100',
lcpLoadStart: '>5000',
lcpLoadEnd: '>5000',
lcpLoadDelay: '>5000',
lcpLoadDuration: '>5000',
}}},
},
'third-party-cookies': {
Expand Down
4 changes: 2 additions & 2 deletions core/audits/predictive-perf.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ class PredictivePerf extends Audit {
pessimisticLCP: lcp.pessimisticEstimate.timeInMs,

roughEstimateOfTTFB: timingSummary.metrics.timeToFirstByte,
roughEstimateOfLCPLoadStart: timingSummary.metrics.lcpLoadStart,
roughEstimateOfLCPLoadEnd: timingSummary.metrics.lcpLoadEnd,
roughEstimateOfLCPLoadStart: timingSummary.metrics.lcpLoadDelay,
roughEstimateOfLCPLoadEnd: timingSummary.metrics.lcpLoadDuration,
};

const score = Audit.computeLogNormalScore(
Expand Down
73 changes: 50 additions & 23 deletions core/computed/metrics/lcp-breakdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,43 +10,70 @@ import {LargestContentfulPaint} from './largest-contentful-paint.js';
import {ProcessedNavigation} from '../processed-navigation.js';
import {TimeToFirstByte} from './time-to-first-byte.js';
import {LCPImageRecord} from '../lcp-image-record.js';
import {NavigationInsights} from '../navigation-insights.js';

// TODO(v13): should use LCPBreakdown insight
/**
* Note: this omits renderDelay for simulated throttling.
*/
class LCPBreakdown {
/**
* @param {LH.Artifacts.MetricComputationDataInput} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<{ttfb: number, loadStart?: number, loadEnd?: number}>}
* @return {Promise<{ttfb: number, loadDelay?: number, loadDuration?: number, renderDelay?: number}>}
*/
static async compute_(data, context) {
const processedNavigation = await ProcessedNavigation.request(data.trace, context);
const observedLcp = processedNavigation.timings.largestContentfulPaint;
if (observedLcp === undefined) {
throw new LighthouseError(LighthouseError.errors.NO_LCP);
}
const timeOrigin = processedNavigation.timestamps.timeOrigin / 1000;
if (data.settings.throttlingMethod === 'simulate') {
const processedNavigation = await ProcessedNavigation.request(data.trace, context);
const observedLcp = processedNavigation.timings.largestContentfulPaint;
if (observedLcp === undefined) {
throw new LighthouseError(LighthouseError.errors.NO_LCP);
}
const timeOrigin = processedNavigation.timestamps.timeOrigin / 1000;

const {timing: ttfb} = await TimeToFirstByte.request(data, context);
const {timing: ttfb} = await TimeToFirstByte.request(data, context);

const lcpRecord = await LCPImageRecord.request(data, context);
if (!lcpRecord) {
return {ttfb};
}
const lcpRecord = await LCPImageRecord.request(data, context);
if (!lcpRecord) {
return {ttfb};
}

// Official LCP^tm. Will be lantern result if simulated, otherwise same as observedLcp.
const {timing: metricLcp} = await LargestContentfulPaint.request(data, context);
const throttleRatio = metricLcp / observedLcp;

// Official LCP^tm. Will be lantern result if simulated, otherwise same as observedLcp.
const {timing: metricLcp} = await LargestContentfulPaint.request(data, context);
const throttleRatio = metricLcp / observedLcp;
const unclampedLoadStart = (lcpRecord.networkRequestTime - timeOrigin) * throttleRatio;
const loadDelay = Math.max(ttfb, Math.min(unclampedLoadStart, metricLcp));

const unclampedLoadStart = (lcpRecord.networkRequestTime - timeOrigin) * throttleRatio;
const loadStart = Math.max(ttfb, Math.min(unclampedLoadStart, metricLcp));
const unclampedLoadEnd = (lcpRecord.networkEndTime - timeOrigin) * throttleRatio;
const loadDuration = Math.max(loadDelay, Math.min(unclampedLoadEnd, metricLcp));

const unclampedLoadEnd = (lcpRecord.networkEndTime - timeOrigin) * throttleRatio;
const loadEnd = Math.max(loadStart, Math.min(unclampedLoadEnd, metricLcp));
return {
ttfb,
loadDelay,
loadDuration,
};
}

const {trace, settings, SourceMaps} = data;
const navInsights = await NavigationInsights.request({trace, settings, SourceMaps}, context);
const lcpBreakdown = navInsights.model.LCPBreakdown;
if (lcpBreakdown instanceof Error) {
throw new LighthouseError(LighthouseError.errors.NO_LCP, {}, {cause: lcpBreakdown});
}

if (!lcpBreakdown.subparts) {
throw new LighthouseError(LighthouseError.errors.NO_LCP);
}

return {
ttfb,
loadStart,
loadEnd,
ttfb: lcpBreakdown.subparts.ttfb.range / 1000,
loadDelay: lcpBreakdown.subparts.loadDelay !== undefined ?
lcpBreakdown.subparts.loadDelay.range / 1000 :
undefined,
loadDuration: lcpBreakdown.subparts.loadDuration !== undefined ?
lcpBreakdown.subparts.loadDuration.range / 1000 :
undefined,
renderDelay: lcpBreakdown.subparts.renderDelay.range / 1000,
};
}
}
Expand Down
43 changes: 33 additions & 10 deletions core/computed/metrics/time-to-first-byte.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@
* SPDX-License-Identifier: Apache-2.0
*/

import {calculateDocFirstByteTs} from '@paulirish/trace_engine/models/trace/insights/Common.js';

import {makeComputedArtifact} from '../computed-artifact.js';
import {NavigationMetric} from './navigation-metric.js';
import {MainResource} from '../main-resource.js';
import {NetworkAnalysis} from '../network-analysis.js';
import {NavigationInsights} from '../navigation-insights.js';
import {TraceEngineResult} from '../trace-engine-result.js';

class TimeToFirstByte extends NavigationMetric {
/**
Expand Down Expand Up @@ -41,18 +45,37 @@ class TimeToFirstByte extends NavigationMetric {
* @return {Promise<LH.Artifacts.Metric>}
*/
static async computeObservedMetric(data, context) {
const mainResource = await MainResource.request(data, context);
if (!mainResource.timing) {
throw new Error('missing timing for main resource');
const {trace, settings, SourceMaps} = data;
const traceEngineResult =
await TraceEngineResult.request({trace, settings, SourceMaps}, context);
const navInsights = await NavigationInsights.request({trace, settings, SourceMaps}, context);
const lcpBreakdown = navInsights.model.LCPBreakdown;

// Defer to LCP breakdown, but if there's no LCP fallback to manual calculation.
if (!(lcpBreakdown instanceof Error) && lcpBreakdown.subparts) {
return {
timing: lcpBreakdown.subparts.ttfb.range / 1000,
timestamp: lcpBreakdown.subparts.ttfb.max,
};
} else if (navInsights.navigation?.args.data?.navigationId) {
const request = traceEngineResult.data.NetworkRequests.byId.get(
navInsights.navigation.args.data.navigationId);
if (!request) {
throw new Error();
}

const timestamp = calculateDocFirstByteTs(request);
if (timestamp === null) {
throw new Error('cannot calculate ttfb');
}

return {
timing: (timestamp - navInsights.navigation.ts) / 1000,
timestamp,
};
}

const {processedNavigation} = data;
const timeOriginTs = processedNavigation.timestamps.timeOrigin;
const timestampMs =
mainResource.timing.requestTime * 1000 + mainResource.timing.receiveHeadersStart;
const timestamp = timestampMs * 1000;
const timing = (timestamp - timeOriginTs) / 1000;
return {timing, timestamp};
throw new Error('cannot determine ttfb');
}
}

Expand Down
5 changes: 3 additions & 2 deletions core/computed/metrics/timing-summary.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,9 @@ class TimingSummary {
cumulativeLayoutShift,
cumulativeLayoutShiftMainFrame,

lcpLoadStart: lcpBreakdown?.loadStart,
lcpLoadEnd: lcpBreakdown?.loadEnd,
lcpLoadDelay: lcpBreakdown?.loadDelay,
lcpLoadDuration: lcpBreakdown?.loadDuration,
lcpRenderDelay: lcpBreakdown?.renderDelay,

timeToFirstByte: ttfb?.timing,
timeToFirstByteTs: ttfb?.timestamp,
Expand Down
4 changes: 2 additions & 2 deletions core/scripts/lantern/collect/golden.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ function makeGolden(log, summary) {
speedIndex: wptMetrics.speedIndex,
largestContentfulPaint: wptMetrics.largestContentfulPaint,
timeToFirstByte: wptMetrics.timeToFirstByte,
lcpLoadStart: wptMetrics.lcpLoadStart,
lcpLoadEnd: wptMetrics.lcpLoadEnd,
lcpLoadDelay: wptMetrics.lcpLoadDelay,
lcpLoadDuration: wptMetrics.lcpLoadDuration,
},
unthrottled: {
tracePath: unthrottled.trace,
Expand Down
29 changes: 17 additions & 12 deletions core/test/audits/__snapshots__/metrics-test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ Object {
"largestContentfulPaintAllFrames": undefined,
"largestContentfulPaintAllFramesTs": undefined,
"largestContentfulPaintTs": undefined,
"lcpLoadEnd": undefined,
"lcpLoadStart": undefined,
"lcpLoadDelay": undefined,
"lcpLoadDuration": undefined,
"lcpRenderDelay": undefined,
"maxPotentialFID": 163,
"observedCumulativeLayoutShift": 0.23332411101386594,
"observedCumulativeLayoutShiftMainFrame": 0.23332411101386594,
Expand Down Expand Up @@ -67,8 +68,9 @@ Object {
"largestContentfulPaintAllFrames": 683,
"largestContentfulPaintAllFramesTs": 23466705983,
"largestContentfulPaintTs": 23466886143,
"lcpLoadEnd": undefined,
"lcpLoadStart": undefined,
"lcpLoadDelay": undefined,
"lcpLoadDuration": undefined,
"lcpRenderDelay": undefined,
"maxPotentialFID": 16,
"observedCumulativeLayoutShift": undefined,
"observedCumulativeLayoutShiftMainFrame": undefined,
Expand Down Expand Up @@ -100,8 +102,8 @@ Object {
"observedTraceEndTs": 23472029453,
"speedIndex": 1583,
"speedIndexTs": 23467606130,
"timeToFirstByte": 565,
"timeToFirstByteTs": 23466588051,
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 trace is just too old for the trace engine to handle wrt ttfb.

"timeToFirstByte": undefined,
"timeToFirstByteTs": undefined,
"totalBlockingTime": 0,
}
`;
Expand All @@ -120,8 +122,9 @@ Object {
"largestContentfulPaintAllFrames": undefined,
"largestContentfulPaintAllFramesTs": undefined,
"largestContentfulPaintTs": undefined,
"lcpLoadEnd": undefined,
"lcpLoadStart": undefined,
"lcpLoadDelay": undefined,
"lcpLoadDuration": undefined,
"lcpRenderDelay": undefined,
"maxPotentialFID": 163,
"observedCumulativeLayoutShift": 0.23332411101386594,
"observedCumulativeLayoutShiftMainFrame": 0.23332411101386594,
Expand Down Expand Up @@ -173,8 +176,9 @@ Object {
"largestContentfulPaintAllFrames": 229,
"largestContentfulPaintAllFramesTs": 376406210378,
"largestContentfulPaintTs": 376406210378,
"lcpLoadEnd": 199,
"lcpLoadStart": 198,
"lcpLoadDelay": undefined,
"lcpLoadDuration": undefined,
"lcpRenderDelay": 59,
"maxPotentialFID": 16,
"observedCumulativeLayoutShift": 0,
"observedCumulativeLayoutShiftMainFrame": 0,
Expand Down Expand Up @@ -226,8 +230,9 @@ Object {
"largestContentfulPaintAllFrames": undefined,
"largestContentfulPaintAllFramesTs": undefined,
"largestContentfulPaintTs": undefined,
"lcpLoadEnd": 972,
"lcpLoadStart": 971,
"lcpLoadDelay": 971,
"lcpLoadDuration": 972,
"lcpRenderDelay": undefined,
"maxPotentialFID": 50,
"observedCumulativeLayoutShift": 0,
"observedCumulativeLayoutShiftMainFrame": 0,
Expand Down
28 changes: 14 additions & 14 deletions core/test/computed/metrics/lcp-breakdown-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function mockData(networkRecords) {

function mockNetworkRecords() {
return [{
requestId: '2',
requestId: 'NAVIGATION_ID',
priority: 'High',
isLinkPreload: false,
networkRequestTime: 0,
Expand All @@ -55,7 +55,7 @@ function mockNetworkRecords() {
frameId: 'ROOT_FRAME',
},
{
requestId: '2:redirect',
requestId: 'NAVIGATION_ID:redirect',
resourceType: 'Document',
priority: 'High',
isLinkPreload: false,
Expand Down Expand Up @@ -110,11 +110,11 @@ describe('LCPBreakdown', () => {
expect(result.ttfb).toBeCloseTo(1245.5, 0.1);
// TODO(15841): investigate difference.
if (process.env.INTERNAL_LANTERN_USE_TRACE !== undefined) {
expect(result.loadStart).toBeCloseTo(3429.1, 0.1);
expect(result.loadEnd).toBeCloseTo(3812.8, 0.1);
expect(result.loadDelay).toBeCloseTo(3429.1, 0.1);
expect(result.loadDuration).toBeCloseTo(3812.8, 0.1);
} else {
expect(result.loadStart).toBeCloseTo(3558.6, 0.1);
expect(result.loadEnd).toBeCloseTo(3956.8, 0.1);
expect(result.loadDelay).toBeCloseTo(3558.6, 0.1);
expect(result.loadDuration).toBeCloseTo(3956.8, 0.1);
}
});

Expand All @@ -132,8 +132,8 @@ describe('LCPBreakdown', () => {
const result = await LCPBreakdown.request(data, {computedCache: new Map()});

expect(result.ttfb).toBeCloseTo(1014.7, 0.1);
expect(result.loadStart).toBeUndefined();
expect(result.loadEnd).toBeUndefined();
expect(result.loadDelay).toBeUndefined();
expect(result.loadDuration).toBeUndefined();
});

it('returns breakdown for image LCP', async () => {
Expand All @@ -143,8 +143,8 @@ describe('LCPBreakdown', () => {
const result = await LCPBreakdown.request(data, {computedCache: new Map()});

expect(result.ttfb).toBeCloseTo(800, 0.1);
expect(result.loadStart).toBeCloseTo(2579.5, 0.1);
expect(result.loadEnd).toBeCloseTo(5804, 0.1);
expect(result.loadDelay).toBeCloseTo(2579.5, 0.1);
expect(result.loadDuration).toBeCloseTo(5804, 0.1);
});

it('returns observed for image LCP', async () => {
Expand All @@ -155,8 +155,8 @@ describe('LCPBreakdown', () => {
const result = await LCPBreakdown.request(data, {computedCache: new Map()});

expect(result.ttfb).toBeCloseTo(800, 0.1);
expect(result.loadStart).toBeCloseTo(2000, 0.1);
expect(result.loadEnd).toBeCloseTo(4500, 0.1);
expect(result.loadDelay).toBeCloseTo(1200, 0.1);
expect(result.loadDuration).toBeCloseTo(2500, 0.1);
});

it('returns breakdown for text LCP', async () => {
Expand All @@ -169,8 +169,8 @@ describe('LCPBreakdown', () => {
const result = await LCPBreakdown.request(data, {computedCache: new Map()});

expect(result.ttfb).toBeCloseTo(800, 0.1);
expect(result.loadStart).toBeUndefined();
expect(result.loadEnd).toBeUndefined();
expect(result.loadDelay).toBeUndefined();
expect(result.loadDuration).toBeUndefined();
});

it('throws if there was no LCP', async () => {
Expand Down
5 changes: 3 additions & 2 deletions core/test/computed/metrics/time-to-first-byte-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ function mockData(networkRecords) {
trace: createTestTrace({
traceEnd: 6000,
largestContentfulPaint: 4500,
networkRecords,
}),
devtoolsLog: networkRecordsToDevtoolsLog(networkRecords),
URL: {
Expand All @@ -37,7 +38,7 @@ function mockData(networkRecords) {

function mockNetworkRecords() {
return [{
requestId: '2',
requestId: 'NAVIGATION_ID',
priority: 'High',
isLinkPreload: false,
networkRequestTime: 0,
Expand All @@ -50,7 +51,7 @@ function mockNetworkRecords() {
responseHeaders: [{name: 'Content-Encoding', value: 'gzip'}],
},
{
requestId: '2:redirect',
requestId: 'NAVIGATION_ID:redirect',
resourceType: 'Document',
priority: 'High',
isLinkPreload: false,
Expand Down
5 changes: 3 additions & 2 deletions core/test/computed/metrics/timing-summary-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ describe('Timing summary', () => {
"largestContentfulPaintAllFrames": 697.751,
"largestContentfulPaintAllFramesTs": 10327885660,
"largestContentfulPaintTs": 10332856184,
"lcpLoadEnd": undefined,
"lcpLoadStart": undefined,
"lcpLoadDelay": undefined,
"lcpLoadDuration": undefined,
"lcpRenderDelay": 5097.946,
"maxPotentialFID": 51.056,
"observedCumulativeLayoutShift": 0.026463014612806653,
"observedCumulativeLayoutShiftMainFrame": 0.0011656245471340055,
Expand Down
Loading