Skip to content

Commit 0326038

Browse files
jackfranklinDevtools-frontend LUCI CQ
authored andcommitted
RPP: fix lcp image request <> navigation relation
The below bug was reported where the LCP Insight was using the wrong image request for the LCP event. This was because the LargestImagePaintHandler tied events to the current navigation, but if the page being loaded has multiple frames, the "current navigation" isn't that helpful and instead we need to track "current navigation per frame" in order to not have data from frame A get mixed up with data from frame B. The bug below is caused by the "current navigation" becoming the loading of an iframe, which happens before the actual LCP image is loaded. That means we mistakenly relate the actual LCP image to an entirely different and unrelated navigation. The fix here is to track the current navigation per frame. Then when we look up and relate the LCP Image request to the navigation, we aren't mistakenly mixing requests across different frames. I also landed an example in the stories repo ChromeDevTools/performance-stories#38 to give us a test case for this and used it in the unit test. Fixed: 384000716 Change-Id: Ica187428545ec1d783ea3a050d0e837ad6f3f729 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6091385 Commit-Queue: Andres Olivares <[email protected]> Auto-Submit: Jack Franklin <[email protected]> Commit-Queue: Jack Franklin <[email protected]> Reviewed-by: Andres Olivares <[email protected]>
1 parent 5f4411c commit 0326038

File tree

5 files changed

+49
-7
lines changed

5 files changed

+49
-7
lines changed

front_end/models/trace/handlers/LargestImagePaintHandler.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
// found in the LICENSE file.
44

55
import type * as Protocol from '../../../generated/protocol.js';
6+
import {describeWithEnvironment} from '../../../testing/EnvironmentHelpers.js';
67
import {TraceLoader} from '../../../testing/TraceLoader.js';
78
import * as Trace from '../trace.js';
89

9-
describe('LargestImagePaintHandler', function() {
10+
describeWithEnvironment('LargestImagePaintHandler', function() {
1011
beforeEach(async () => {
1112
Trace.Handlers.ModelHandlers.LargestImagePaint.reset();
1213
});
@@ -24,4 +25,21 @@ describe('LargestImagePaintHandler', function() {
2425
assert.strictEqual(imageForLCP?.args.data?.DOMNodeId, 10 as Protocol.DOM.BackendNodeId);
2526
assert.strictEqual(imageForLCP?.args.data?.imageUrl, 'https://via.placeholder.com/2000.jpg');
2627
});
28+
29+
it('is able to identify the LCP image request for each navigation', async function() {
30+
// The handler depends on Meta + Network requests, let's just execute
31+
// all of them rather than call them individually.
32+
const {parsedTrace} = await TraceLoader.traceEngine(this, 'lcp-multiple-frames.json.gz');
33+
34+
const {mainFrameNavigations} = parsedTrace.Meta;
35+
// There is only one main frame navigation in this trace.
36+
assert.lengthOf(mainFrameNavigations, 1);
37+
const mainNavigation = mainFrameNavigations.at(0);
38+
assert.isOk(mainNavigation);
39+
40+
const {lcpRequestByNavigation} = parsedTrace.LargestImagePaint;
41+
const lcpRequest = lcpRequestByNavigation.get(mainNavigation);
42+
assert.isOk(lcpRequest);
43+
assert.strictEqual(lcpRequest.args.data.url, 'https://placehold.co/1000.jpg');
44+
});
2745
});

front_end/models/trace/handlers/LargestImagePaintHandler.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,26 +21,32 @@ import type {HandlerName} from './types.js';
2121
* `LargestContentfulPaint::Candidate` will have. So, when we find an image
2222
* paint candidate, we can store it, keying it on the node ID.
2323
* Then, when it comes to finding the network request for an LCP image, we can
24-
*
2524
* use the nodeId from the LCP candidate to find the image candidate. That image
2625
* candidate also contains a `imageUrl` property, which will have the full URL
2726
* to the image.
2827
**/
2928
const imageByDOMNodeId = new Map<Protocol.DOM.BackendNodeId, Types.Events.LargestImagePaintCandidate>();
3029
const lcpRequestByNavigation = new Map<Types.Events.NavigationStart|null, Types.Events.SyntheticNetworkRequest>();
3130
const lcpPaintEventByNavigation = new Map<Types.Events.NavigationStart|null, Types.Events.LargestImagePaintCandidate>();
32-
let currentNavigation: Types.Events.NavigationStart|null;
31+
32+
/**
33+
* We track the latest navigation that happened because we want to relate each
34+
* LargestImagePaintCandidate to the navigation it occurred after, but we have
35+
* to track navigations per-frame to avoid us reading a navigation from one
36+
* frame and treating it as the latest navigation across all frames.
37+
*/
38+
const lastNavigationPerFrame = new Map<string, Types.Events.NavigationStart>();
3339

3440
export function reset(): void {
3541
imageByDOMNodeId.clear();
3642
lcpRequestByNavigation.clear();
3743
lcpPaintEventByNavigation.clear();
38-
currentNavigation = null;
44+
lastNavigationPerFrame.clear();
3945
}
4046

4147
export function handleEvent(event: Types.Events.Event): void {
42-
if (Types.Events.isNavigationStart(event)) {
43-
currentNavigation = event;
48+
if (Types.Events.isNavigationStart(event) && event.args.frame) {
49+
lastNavigationPerFrame.set(event.args.frame, event);
4450
return;
4551
}
4652

@@ -53,7 +59,13 @@ export function handleEvent(event: Types.Events.Event): void {
5359
}
5460

5561
imageByDOMNodeId.set(event.args.data.DOMNodeId, event);
56-
lcpPaintEventByNavigation.set(currentNavigation, event);
62+
63+
// Now relate this LargestImagePaintCandidate event to the last navigation
64+
// that occurred within the same frame.
65+
const navigationForEvent = lastNavigationPerFrame.get(event.args.frame);
66+
if (navigationForEvent) {
67+
lcpPaintEventByNavigation.set(navigationForEvent, event);
68+
}
5769
}
5870

5971
export async function finalize(): Promise<void> {

front_end/panels/timeline/fixtures/traces/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ copy_to_gen("traces") {
4646
"large-recalc-style.json.gz",
4747
"lcp-images-rasterizer.json.gz",
4848
"lcp-images.json.gz",
49+
"lcp-multiple-frames.json.gz",
4950
"lcp-web-font.json.gz",
5051
"load-simple.json.gz",
5152
"mainWasm_profile.json.gz",

front_end/panels/timeline/fixtures/traces/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,14 @@ Generate from a recording of [this HTML file](https://gist.github.com/adamraine/
228228
4. Reload the page
229229
5. Click the button once
230230
6. End recording
231+
232+
### lcp-multiple-frames
233+
234+
Generated from [lcp-iframes story](https://github.com/ChromeDevTools/performance-stories/tree/main/lcp-iframes).
235+
236+
Contains a page load that has two frames (main frame + iframe). There are two images loaded in each:
237+
238+
- the iframe loads placeholder.co/50.jpg and placeholder.co/2000.jpg
239+
- the main frame loads placeholder.co/100.jpg and placeholder.co/1000.jpg
240+
241+
This trace is used to verify the fix for a bug [crbug.com/384000716] where we incorrectly associated image requests to the wrong navigation when calculating the LCP image.
Binary file not shown.

0 commit comments

Comments
 (0)