Skip to content

Commit b0d8d4a

Browse files
Adriana IxbaDevtools-frontend LUCI CQ
authored andcommitted
[RPP]: Move third parties summaries extraction to helper
This also gets third party summaries based on a tracebounds Bug:372881026,374192107 Change-Id: I6c2c965aea67033ee190bd8f55a25bb2e084f42c Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/5939714 Commit-Queue: Adriana Ixba <[email protected]> Reviewed-by: Connor Clark <[email protected]>
1 parent ee8942e commit b0d8d4a

File tree

9 files changed

+347
-189
lines changed

9 files changed

+347
-189
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,6 +1028,7 @@ grd_files_debug_sources = [
10281028
"front_end/models/trace/extras/FilmStrip.js",
10291029
"front_end/models/trace/extras/MainThreadActivity.js",
10301030
"front_end/models/trace/extras/Metadata.js",
1031+
"front_end/models/trace/extras/ThirdParties.js",
10311032
"front_end/models/trace/extras/URLForEntry.js",
10321033
"front_end/models/trace/handlers/AnimationHandler.js",
10331034
"front_end/models/trace/handlers/AuctionWorkletsHandler.js",

front_end/models/trace/extras/BUILD.gn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ devtools_module("extras") {
1313
"FilmStrip.ts",
1414
"MainThreadActivity.ts",
1515
"Metadata.ts",
16+
"ThirdParties.ts",
1617
"URLForEntry.ts",
1718
]
1819

@@ -44,6 +45,7 @@ ts_library("unittests") {
4445
"FilmStrip.test.ts",
4546
"MainThreadActivity.test.ts",
4647
"Metadata.test.ts",
48+
"ThirdParties.test.ts",
4749
"URLForEntry.test.ts",
4850
]
4951

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright 2024 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import {describeWithEnvironment} from '../../../testing/EnvironmentHelpers.js';
6+
import {TraceLoader} from '../../../testing/TraceLoader.js';
7+
import * as Trace from '../trace.js';
8+
9+
describeWithEnvironment('ThirdParties', function() {
10+
describe('Entities', function() {
11+
it('correctly makes up entities', async function() {
12+
const expectedEntities = new Map<string, string>([
13+
['http://localhost:8080/', 'localhost'],
14+
['https://fonts.googleapis.com/css2?family=Orelega+One&display=swap', 'googleapis.com'],
15+
['https://emp.bbci.co.uk/emp/bump-4/bump-4.js', 'bbci.co.uk'],
16+
['http://localhost:8080/blocking.js', 'localhost'],
17+
['https://fonts.gstatic.com/s/orelegaone/v1/3qTpojOggD2XtAdFb-QXZFt93kY.woff2', 'gstatic.com'],
18+
['chrome-extension://chromeextension/something/exciting.js', 'chromeextension'],
19+
]);
20+
21+
for (const [url, expectedEntity] of expectedEntities.entries()) {
22+
const gotEntity =
23+
Trace.Extras.ThirdParties.makeUpEntity(new Map<string, Trace.Extras.ThirdParties.Entity>(), url)?.name ??
24+
'';
25+
assert.deepEqual(gotEntity, expectedEntity);
26+
}
27+
});
28+
it('coreectly makes up chrome extension entity', async function() {
29+
const url = 'chrome-extension://chromeextension/something/exciting.js';
30+
const gotEntity =
31+
Trace.Extras.ThirdParties.makeUpEntity(new Map<string, Trace.Extras.ThirdParties.Entity>(), url);
32+
assert.exists(gotEntity);
33+
34+
assert.deepEqual(gotEntity.name, 'chromeextension');
35+
assert.deepEqual(gotEntity.category, 'Chrome Extension');
36+
assert.deepEqual(gotEntity.homepage, 'https://chromewebstore.google.com/detail/chromeextension');
37+
});
38+
it('gets correct entitiesByRequest', async function() {
39+
const {parsedTrace} = await TraceLoader.traceEngine(this, 'load-simple.json.gz');
40+
const reqs = parsedTrace.NetworkRequests.byTime;
41+
42+
const got = Trace.Extras.ThirdParties.getEntitiesByRequest(reqs);
43+
const gotEntityByRequest = [...got.entityByRequest.entries()].map(([req, entity]) => {
44+
return [req.args.data.url, entity.name];
45+
});
46+
47+
assert.deepEqual(gotEntityByRequest, [
48+
['http://localhost:8080/', 'localhost'],
49+
['https://fonts.googleapis.com/css2?family=Orelega+One&display=swap', 'Google Fonts'],
50+
['http://localhost:8080/styles.css', 'localhost'],
51+
['http://localhost:8080/blocking.js', 'localhost'],
52+
['http://localhost:8080/module.js', 'localhost'],
53+
['https://fonts.gstatic.com/s/orelegaone/v1/3qTpojOggD2XtAdFb-QXZFt93kY.woff2', 'Google Fonts'],
54+
]);
55+
});
56+
});
57+
describe('byTraceBounds', function() {
58+
it('full trace bounds', async function() {
59+
const {parsedTrace} = await TraceLoader.traceEngine(this, 'load-simple.json.gz');
60+
const reqs = parsedTrace.NetworkRequests.byTime;
61+
62+
const {entityByRequest} = Trace.Extras.ThirdParties.getSummariesAndEntitiesForTraceBounds(
63+
parsedTrace, parsedTrace.Meta.traceBounds, reqs);
64+
65+
const gotEntityByRequest = [...entityByRequest.entries()].map(([req, entity]) => {
66+
return [req.args.data.url, entity.name];
67+
});
68+
69+
assert.deepEqual(gotEntityByRequest, [
70+
['http://localhost:8080/', 'localhost'],
71+
['https://fonts.googleapis.com/css2?family=Orelega+One&display=swap', 'Google Fonts'],
72+
['http://localhost:8080/styles.css', 'localhost'],
73+
['http://localhost:8080/blocking.js', 'localhost'],
74+
['http://localhost:8080/module.js', 'localhost'],
75+
['https://fonts.gstatic.com/s/orelegaone/v1/3qTpojOggD2XtAdFb-QXZFt93kY.woff2', 'Google Fonts'],
76+
]);
77+
});
78+
it('partial trace bounds', async function() {
79+
const {parsedTrace} = await TraceLoader.traceEngine(this, 'load-simple.json.gz');
80+
const reqs = parsedTrace.NetworkRequests.byTime;
81+
82+
// Font requests of load-simple.json.gz begin & end before/after this bounds.
83+
const min = Trace.Types.Timing.MicroSeconds(1634222300000);
84+
const max = Trace.Types.Timing.MicroSeconds(1634222320000);
85+
const middle = {min, max, range: Trace.Types.Timing.MicroSeconds(max - min)};
86+
87+
const {entityByRequest} =
88+
Trace.Extras.ThirdParties.getSummariesAndEntitiesForTraceBounds(parsedTrace, middle, reqs);
89+
const gotEntityByRequest = [...entityByRequest.entries()].map(([req, entity]) => {
90+
return [req.args.data.url, entity.name];
91+
});
92+
93+
// Only these localhost requests overlap traceBounds.
94+
assert.deepEqual(gotEntityByRequest, [
95+
['http://localhost:8080/', 'localhost'],
96+
['http://localhost:8080/styles.css', 'localhost'],
97+
['http://localhost:8080/blocking.js', 'localhost'],
98+
['http://localhost:8080/module.js', 'localhost'],
99+
]);
100+
});
101+
it('no requests within trace bounds', async function() {
102+
const {parsedTrace} = await TraceLoader.traceEngine(this, 'load-simple.json.gz');
103+
const reqs = parsedTrace.NetworkRequests.byTime;
104+
105+
const min = Trace.Types.Timing.MicroSeconds(1634230000000);
106+
const max = Trace.Types.Timing.MicroSeconds(1634231000000);
107+
const middle = {min, max, range: Trace.Types.Timing.MicroSeconds(max - min)};
108+
109+
const {entityByRequest} =
110+
Trace.Extras.ThirdParties.getSummariesAndEntitiesForTraceBounds(parsedTrace, middle, reqs);
111+
const gotEntityByRequest = [...entityByRequest.entries()].map(([req, entity]) => {
112+
return [req.args.data.url, entity.name];
113+
});
114+
assert.deepEqual(gotEntityByRequest, []);
115+
});
116+
});
117+
});
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
// Copyright 2024 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import * as ThirdPartyWeb from '../../../third_party/third-party-web/third-party-web.js';
6+
import type * as Handlers from '../handlers/handlers.js';
7+
import * as Helpers from '../helpers/helpers.js';
8+
import * as Types from '../types/types.js';
9+
10+
import * as URLForEntry from './URLForEntry.js';
11+
12+
export type Entity = typeof ThirdPartyWeb.ThirdPartyWeb.entities[number];
13+
14+
export interface Summary {
15+
transferSize: number;
16+
mainThreadTime: Types.Timing.MicroSeconds;
17+
}
18+
19+
export interface SummaryMaps {
20+
byEntity: Map<Entity, Summary>;
21+
byRequest: Map<Types.Events.SyntheticNetworkRequest, Summary>;
22+
requestsByEntity: Map<Entity, Types.Events.SyntheticNetworkRequest[]>;
23+
}
24+
25+
/**
26+
* Returns the origin portion of a Chrome extension URL.
27+
*/
28+
function getChromeExtensionOrigin(url: URL): string {
29+
return url.protocol + '//' + url.host;
30+
}
31+
32+
function makeUpChromeExtensionEntity(entityCache: Map<string, Entity>, url: string, extensionName?: string): Entity {
33+
const parsedUrl = new URL(url);
34+
const origin = getChromeExtensionOrigin(parsedUrl);
35+
const host = new URL(origin).host;
36+
const name = extensionName || host;
37+
38+
const cachedEntity = entityCache.get(origin);
39+
if (cachedEntity) {
40+
return cachedEntity;
41+
}
42+
43+
const chromeExtensionEntity = {
44+
name,
45+
company: name,
46+
category: 'Chrome Extension',
47+
homepage: 'https://chromewebstore.google.com/detail/' + host,
48+
categories: [],
49+
domains: [],
50+
averageExecutionTime: 0,
51+
totalExecutionTime: 0,
52+
totalOccurrences: 0,
53+
};
54+
55+
entityCache.set(origin, chromeExtensionEntity);
56+
return chromeExtensionEntity;
57+
}
58+
59+
export function makeUpEntity(entityCache: Map<string, Entity>, url: string): Entity|undefined {
60+
if (url.startsWith('chrome-extension:')) {
61+
return makeUpChromeExtensionEntity(entityCache, url);
62+
}
63+
64+
// Make up an entity only for valid http/https URLs.
65+
if (!url.startsWith('http')) {
66+
return;
67+
}
68+
69+
// NOTE: Lighthouse uses a tld database to determine the root domain, but here
70+
// we are using third party web's database. Doesn't really work for the case of classifying
71+
// domains 3pweb doesn't know about, so it will just give us a guess.
72+
const rootDomain = ThirdPartyWeb.ThirdPartyWeb.getRootDomain(url);
73+
if (!rootDomain) {
74+
return;
75+
}
76+
77+
if (entityCache.has(rootDomain)) {
78+
return entityCache.get(rootDomain);
79+
}
80+
81+
const unrecognizedEntity = {
82+
name: rootDomain,
83+
company: rootDomain,
84+
category: '',
85+
categories: [],
86+
domains: [rootDomain],
87+
averageExecutionTime: 0,
88+
totalExecutionTime: 0,
89+
totalOccurrences: 0,
90+
isUnrecognized: true,
91+
};
92+
entityCache.set(rootDomain, unrecognizedEntity);
93+
return unrecognizedEntity;
94+
}
95+
96+
function getSelfTimeByUrl(
97+
parsedTrace: Handlers.Types.ParsedTrace, bounds: Types.Timing.TraceWindowMicroSeconds): Map<string, number> {
98+
const selfTimeByUrl = new Map<string, number>();
99+
100+
for (const process of parsedTrace.Renderer.processes.values()) {
101+
if (!process.isOnMainFrame) {
102+
continue;
103+
}
104+
105+
for (const thread of process.threads.values()) {
106+
if (thread.name === 'CrRendererMain') {
107+
if (!thread.tree) {
108+
break;
109+
}
110+
111+
for (const event of thread.entries) {
112+
if (!Helpers.Timing.eventIsInBounds(event, bounds)) {
113+
continue;
114+
}
115+
116+
const node = parsedTrace.Renderer.entryToNode.get(event);
117+
if (!node || !node.selfTime) {
118+
continue;
119+
}
120+
121+
const url = URLForEntry.getNonResolved(parsedTrace as Handlers.Types.ParsedTrace, event);
122+
if (!url) {
123+
continue;
124+
}
125+
126+
selfTimeByUrl.set(url, node.selfTime + (selfTimeByUrl.get(url) ?? 0));
127+
}
128+
}
129+
}
130+
}
131+
132+
return selfTimeByUrl;
133+
}
134+
135+
export function getEntitiesByRequest(requests: Types.Events.SyntheticNetworkRequest[]):
136+
{entityByRequest: Map<Types.Events.SyntheticNetworkRequest, Entity>, madeUpEntityCache: Map<string, Entity>} {
137+
const entityByRequest = new Map<Types.Events.SyntheticNetworkRequest, Entity>();
138+
const madeUpEntityCache = new Map<string, Entity>();
139+
for (const request of requests) {
140+
const url = request.args.data.url;
141+
const entity = ThirdPartyWeb.ThirdPartyWeb.getEntity(url) ?? makeUpEntity(madeUpEntityCache, url);
142+
if (entity) {
143+
entityByRequest.set(request, entity);
144+
}
145+
}
146+
return {entityByRequest, madeUpEntityCache};
147+
}
148+
149+
function getSummaryMap(
150+
requests: Types.Events.SyntheticNetworkRequest[],
151+
entityByRequest: Map<Types.Events.SyntheticNetworkRequest, Entity>,
152+
selfTimeByUrl: Map<string, number>): SummaryMaps {
153+
const byRequest = new Map<Types.Events.SyntheticNetworkRequest, Summary>();
154+
const byEntity = new Map<Entity, Summary>();
155+
const defaultSummary: Summary = {transferSize: 0, mainThreadTime: Types.Timing.MicroSeconds(0)};
156+
157+
for (const request of requests) {
158+
const urlSummary = byRequest.get(request) || {...defaultSummary};
159+
urlSummary.transferSize += request.args.data.encodedDataLength;
160+
urlSummary.mainThreadTime =
161+
Types.Timing.MicroSeconds(urlSummary.mainThreadTime + (selfTimeByUrl.get(request.args.data.url) ?? 0));
162+
byRequest.set(request, urlSummary);
163+
}
164+
165+
// Map each request's stat to a particular entity.
166+
const requestsByEntity = new Map<Entity, Types.Events.SyntheticNetworkRequest[]>();
167+
for (const [request, requestSummary] of byRequest.entries()) {
168+
const entity = entityByRequest.get(request);
169+
if (!entity) {
170+
byRequest.delete(request);
171+
continue;
172+
}
173+
174+
const entitySummary = byEntity.get(entity) || {...defaultSummary};
175+
entitySummary.transferSize += requestSummary.transferSize;
176+
entitySummary.mainThreadTime =
177+
Types.Timing.MicroSeconds(entitySummary.mainThreadTime + requestSummary.mainThreadTime);
178+
byEntity.set(entity, entitySummary);
179+
180+
const entityRequests = requestsByEntity.get(entity) || [];
181+
entityRequests.push(request);
182+
requestsByEntity.set(entity, entityRequests);
183+
}
184+
185+
return {byEntity, byRequest, requestsByEntity};
186+
}
187+
188+
export function getSummariesAndEntitiesForTraceBounds(
189+
parsedTrace: Handlers.Types.ParsedTrace, traceBounds: Types.Timing.TraceWindowMicroSeconds,
190+
networkRequests: Types.Events.SyntheticNetworkRequest[]): {
191+
summaries: SummaryMaps,
192+
entityByRequest: Map<Types.Events.SyntheticNetworkRequest, Entity>,
193+
madeUpEntityCache: Map<string, Entity>,
194+
} {
195+
// Ensure we only handle requests that are within the given traceBounds.
196+
const reqs = networkRequests.filter(event => {
197+
return Helpers.Timing.eventIsInBounds(event, traceBounds);
198+
});
199+
200+
const {entityByRequest, madeUpEntityCache} = getEntitiesByRequest(reqs);
201+
202+
const selfTimeByUrl = getSelfTimeByUrl(parsedTrace, traceBounds);
203+
// TODO(crbug.com/352244718): re-work to still collect main thread activity if no request is present
204+
const summaries = getSummaryMap(reqs, entityByRequest, selfTimeByUrl);
205+
206+
return {summaries, entityByRequest, madeUpEntityCache};
207+
}

front_end/models/trace/extras/extras.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ export * as FetchNodes from './FetchNodes.js';
66
export * as FilmStrip from './FilmStrip.js';
77
export * as MainThreadActivity from './MainThreadActivity.js';
88
export * as Metadata from './Metadata.js';
9+
export * as ThirdParties from './ThirdParties.js';
910
export * as URLForEntry from './URLForEntry.js';

front_end/models/trace/helpers/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ devtools_module("helpers") {
2323
"../../../core/platform:bundle",
2424
"../../../core/root:bundle",
2525
"../../../models/cpu_profile:bundle",
26+
"../../../third_party/third-party-web:bundle",
2627
"../types:bundle",
2728
]
2829
}

0 commit comments

Comments
 (0)