Skip to content

Commit 465fc4e

Browse files
Connor ClarkDevtools-frontend LUCI CQ
authored andcommitted
[RPP] Calculate TTFB even for older traces
TTFB relied on an M116 addition (receiveHeadersStart), but receiveHeadersEnd is close enough for our purposes. While broader support for historical traces is nice, this is especially useful since many of our test fixtures are pre-M116. Bug: none Change-Id: I44eb0da2cf7596b0027bfcda7e80e42e5bc393c7 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6876549 Auto-Submit: Connor Clark <cjamcl@chromium.org> Reviewed-by: Paul Irish <paulirish@chromium.org> Commit-Queue: Connor Clark <cjamcl@chromium.org>
1 parent 6458dfd commit 465fc4e

8 files changed

Lines changed: 69 additions & 29 deletions

File tree

front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.snapshot.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,11 +495,13 @@ CPU throttling: none
495495
Network throttling: none
496496
Metrics:
497497
- LCP: 118 ms, event: (eventKey: r-1802, ts: 1020034953358)
498+
- TTFB: 31.3 ms, bounds: {min: 1020034834921, max: 1020034866264}
499+
- Render delay: 87 ms, bounds: {min: 1020034866264, max: 1020034953358}
498500
- CLS: 0
499501
Available insights:
500502
- insight name: LCPBreakdown
501503
description: Each [subpart has specific improvement strategies](https://web.dev/articles/optimize-lcp#lcp-breakdown). Ideally, most of the LCP time should be spent on loading the resources, not within delays.
502-
relevant trace bounds: {min: 1020034834921, max: 1020036087961}
504+
relevant trace bounds: {min: 1020034834921, max: 1020034953358}
503505
example question: Help me optimize my LCP score
504506
example question: Which LCP phase was most problematic?
505507
example question: What can I do to reduce the LCP time for this page load?

front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.test.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,6 @@ describeWithEnvironment('PerformanceTraceFormatter', () => {
3434

3535
describe('formatTraceSummary', () => {
3636
it('web-dev.json.gz', async function() {
37-
// TODO(b/425270067): in LCPBreakdown, `anyValuesNaN` is true, so the summary
38-
// does not include LCP phases. Investigate this.
39-
// LOG: Object{min: 1020034834921, max: NaN, range: NaN, label: 'Time to first byte'}
40-
// LOG: Object{min: NaN, max: 1020034953358, range: NaN, label: 'Element render delay'}
4137
const {formatter} = await createFormatter(this, 'web-dev.json.gz');
4238
const output = formatter.formatTraceSummary();
4339
snapshotTester.assert(this, output);

front_end/models/trace/LanternComputationData.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ function createLanternRequest(
102102
// These two timings are not included in the trace.
103103
workerFetchStart: -1,
104104
workerRespondWithSettled: -1,
105+
receiveHeadersStart: -1,
105106
...request.args.data.timing,
106107
} :
107108
undefined;

front_end/models/trace/insights/Common.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,3 +411,24 @@ export function estimateCompressionRatioForScript(script: Handlers.ModelHandlers
411411
const compressionRatio = compressedSize / contentLength;
412412
return compressionRatio;
413413
}
414+
415+
export function calculateDocFirstByteTs(docRequest: Types.Events.SyntheticNetworkRequest): Types.Timing.Micro|null {
416+
if (docRequest.args.data.protocol === 'file') {
417+
// file: requests do not have timings
418+
return docRequest.ts;
419+
}
420+
421+
const timing = docRequest.args.data.timing;
422+
if (!timing) {
423+
// Older traces do not have timings.
424+
return null;
425+
}
426+
427+
// Time that first byte (headers) are received.
428+
// For older traces, receiveHeadersStart can be missing (ex: web.dev.json.gz).
429+
// In that case use the headers end timing, which should be pretty close to when
430+
// the headers start.
431+
return Types.Timing.Micro(
432+
Helpers.Timing.secondsToMicro(timing.requestTime) +
433+
Helpers.Timing.milliToMicro(timing.receiveHeadersStart ?? timing.receiveHeadersEnd));
434+
}

front_end/models/trace/insights/LCPBreakdown.test.ts

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,20 @@ describeWithEnvironment('LCPBreakdown', function() {
2222
assert.deepEqual(actual, {ttfb: wantTtfb, renderDelay: wantRenderDelay});
2323
});
2424

25+
it('calculates text lcp breakdown (doc missing receiveHeadersStart)', async () => {
26+
const {data, insights} = await processTrace(this, 'web-dev.json.gz');
27+
const firstNav = getFirstOrError(data.Meta.navigationsByNavigationId.values());
28+
const insight = getInsightOrError('LCPBreakdown', insights, firstNav);
29+
30+
assert.strictEqual(insight.lcpMs, 118.437);
31+
32+
const wantTtfb = Types.Timing.Micro(31343);
33+
const wantRenderDelay = Types.Timing.Micro(87094);
34+
assert.exists(insight.subparts);
35+
const actual = Object.fromEntries(Object.entries(insight.subparts).map(([key, value]) => [key, value.range]));
36+
assert.deepEqual(actual, {ttfb: wantTtfb, renderDelay: wantRenderDelay});
37+
});
38+
2539
it('calculates image lcp breakdown', async () => {
2640
const {data, insights} = await processTrace(this, 'lcp-images.json.gz');
2741
const firstNav = getFirstOrError(data.Meta.navigationsByNavigationId.values());
@@ -42,6 +56,26 @@ describeWithEnvironment('LCPBreakdown', function() {
4256
assert.deepEqual(subparts, {ttfb: '6.94', loadTime: '12.10', loadDelay: '33.74', renderDelay: '56.85'});
4357
});
4458

59+
it('calculates image lcp breakdown (doc missing receiveHeadersStart)', async () => {
60+
const {data, insights} = await processTrace(this, 'multiple-navigations.json.gz');
61+
const firstNav = getFirstOrError(data.Meta.navigationsByNavigationId.values());
62+
const insight = getInsightOrError('LCPBreakdown', insights, firstNav);
63+
64+
assert.strictEqual(insight.lcpMs, 118.677);
65+
66+
if (!insight.subparts) {
67+
throw new Error('No LCP subparts');
68+
}
69+
70+
const subparts = {
71+
ttfb: Helpers.Timing.microToMilli(insight.subparts.ttfb?.range).toFixed(2),
72+
loadTime: Helpers.Timing.microToMilli(insight.subparts.loadDuration?.range as Types.Timing.Micro).toFixed(2),
73+
loadDelay: Helpers.Timing.microToMilli(insight.subparts.loadDelay?.range as Types.Timing.Micro).toFixed(2),
74+
renderDelay: Helpers.Timing.microToMilli(insight.subparts.renderDelay?.range).toFixed(2),
75+
};
76+
assert.deepEqual(subparts, {ttfb: '61.47', loadTime: '0.19', loadDelay: '47.74', renderDelay: '9.28'});
77+
});
78+
4579
describe('warnings', function() {
4680
it('warns when there is no lcp', async () => {
4781
const {data, insights} = await processTrace(this, 'user-timings.json.gz');
@@ -63,12 +97,4 @@ describeWithEnvironment('LCPBreakdown', function() {
6397
assert.strictEqual(insight.warnings?.[0], 'NO_DOCUMENT_REQUEST');
6498
});
6599
});
66-
67-
it('can handle old traces with missing data and return null for breakdowns of the subparts', async () => {
68-
const {data, insights} = await processTrace(this, 'multiple-navigations.json.gz');
69-
const firstNav = getFirstOrError(data.Meta.navigationsByNavigationId.values());
70-
const insight = getInsightOrError('LCPBreakdown', insights, firstNav);
71-
// This insight has invalid subparts, so we expect the value to be undefined.
72-
assert.isUndefined(insight.subparts);
73-
});
74100
});

front_end/models/trace/insights/LCPBreakdown.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import * as Handlers from '../handlers/handlers.js';
88
import * as Helpers from '../helpers/helpers.js';
99
import * as Types from '../types/types.js';
1010

11+
import {calculateDocFirstByteTs} from './Common.js';
1112
import {
1213
InsightCategory,
1314
InsightKeys,
@@ -113,15 +114,9 @@ function determineSubparts(
113114
nav: Types.Events.NavigationStart, docRequest: Types.Events.SyntheticNetworkRequest,
114115
lcpEvent: Types.Events.LargestContentfulPaintCandidate,
115116
lcpRequest: Types.Events.SyntheticNetworkRequest|undefined): LCPSubparts|null {
116-
const docReqTiming = docRequest.args.data.timing;
117-
118-
let firstDocByteTs;
119-
if (docReqTiming) {
120-
firstDocByteTs = Types.Timing.Micro(
121-
Helpers.Timing.secondsToMicro(docReqTiming.requestTime) +
122-
Helpers.Timing.milliToMicro(docReqTiming.receiveHeadersStart));
123-
} else {
124-
firstDocByteTs = docRequest.ts; // file:
117+
const firstDocByteTs = calculateDocFirstByteTs(docRequest);
118+
if (firstDocByteTs === null) {
119+
return null;
125120
}
126121

127122
const ttfb = Helpers.Timing.traceWindowFromMicroSeconds(nav.ts, firstDocByteTs) as Subpart;

front_end/models/trace/insights/LCPDiscovery.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as Handlers from '../handlers/handlers.js';
77
import * as Helpers from '../helpers/helpers.js';
88
import * as Types from '../types/types.js';
99

10+
import {calculateDocFirstByteTs} from './Common.js';
1011
import {
1112
type Checklist,
1213
InsightCategory,
@@ -135,11 +136,8 @@ export function generateInsight(
135136

136137
const imageLoadingAttr = lcpEvent.args.data?.loadingAttr;
137138
const imageFetchPriorityHint = lcpRequest?.args.data.fetchPriorityHint;
138-
// This is the earliest discovery time an LCP request could have - it's TTFB.
139-
const earliestDiscoveryTime = docRequest?.args.data.timing ?
140-
Helpers.Timing.secondsToMicro(docRequest.args.data.timing.requestTime) +
141-
Helpers.Timing.milliToMicro(docRequest.args.data.timing.receiveHeadersStart) :
142-
undefined;
139+
// This is the earliest discovery time an LCP request could have - it's TTFB (as an absolute timestamp).
140+
const earliestDiscoveryTime = calculateDocFirstByteTs(docRequest);
143141

144142
const priorityHintFound = imageFetchPriorityHint === 'high';
145143

front_end/models/trace/types/TraceEvents.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ export interface SyntheticNetworkRequest extends Complete, SyntheticBased<Phase.
389389
*
390390
* Note, this is not the same as URL.protocol.
391391
*
392-
* Example values (not exhaustive): http/0.9, http/1.0, http/1.1, http, h2, h3-Q050, data, blob
392+
* Example values (not exhaustive): http/0.9, http/1.0, http/1.1, http, h2, h3-Q050, data, blob, file
393393
*/
394394
protocol: string,
395395
redirects: SyntheticNetworkRedirect[],
@@ -1115,7 +1115,8 @@ interface ResourceReceiveResponseTimingData {
11151115
pushEnd: Milli;
11161116
pushStart: Milli;
11171117
receiveHeadersEnd: Milli;
1118-
receiveHeadersStart: Milli;
1118+
/** M116. */
1119+
receiveHeadersStart?: Milli;
11191120
/** When the network service is about to handle a request, ie. just before going to the HTTP cache or going to the network for DNS/connection setup. */
11201121
requestTime: Seconds;
11211122
sendEnd: Milli;

0 commit comments

Comments
 (0)