-
Notifications
You must be signed in to change notification settings - Fork 9.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
core(lantern): create network graph from trace (experimental) #16026
Changes from 1 commit
b4fa823
a487855
acaa25c
5bde7b5
aca1615
6a50568
171f7b7
bc5ebbf
318c6de
037e690
4e07d3a
5a916b2
9802496
f955dd6
0c27d0b
2b7d434
53b713d
2b19b21
0ccc87e
300ed78
96d2ed7
9d8969e
78e66e5
4166ca8
c2927c3
552dda9
c0c75d0
862cda9
32944ce
d23acb0
dff6fe5
b5c0ec8
09ff562
8db8c7c
bd244c7
b689180
ffc56e2
7d637a4
e4196d8
11c0b5b
fc6b342
841e534
61b52b7
84ef26a
1644637
d6dd650
c0e6226
40d0f2a
183e50d
9fdcbab
30460e7
025fd52
8c938e4
976a91c
9811633
20f72d0
6edccfe
a436e25
6b85157
6126b22
b0a78af
007a6f7
34316d5
bf7b5d2
df982f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -593,14 +593,13 @@ class PageDependencyGraph { | |
} | ||
|
||
/** | ||
* @param {LH.TraceEvent[]} mainThreadEvents | ||
* @param {LH.Trace} trace | ||
* @param {LH.Artifacts.TraceEngineResult} traceEngineResult | ||
* @param {LH.Artifacts.URL} URL | ||
* @return {Map<number, number[]>} | ||
*/ | ||
static async createGraphFromTrace(mainThreadEvents, trace, traceEngineResult, URL) { | ||
// TODO: trace engine should handle this. | ||
static _findServiceWorkerThreads(trace) { | ||
// TODO: trace engine should provide a map of service workers, like it does for workers. | ||
const serviceWorkerThreads = new Map(); | ||
|
||
for (const event of trace.traceEvents) { | ||
if (!(event.name === 'thread_name' && event.args.name === 'ServiceWorker thread')) { | ||
continue; | ||
|
@@ -614,112 +613,157 @@ class PageDependencyGraph { | |
} | ||
} | ||
|
||
/** @type {Lantern.NetworkRequest[]} */ | ||
const lanternRequests = []; | ||
return serviceWorkerThreads; | ||
} | ||
|
||
for (const request of traceEngineResult.data.NetworkRequests.byTime) { | ||
if (request.args.data.connectionId === undefined || | ||
request.args.data.connectionReused === undefined) { | ||
throw new Error('Trace is too old'); | ||
} | ||
/** | ||
* @param {LH.Artifacts.TraceEngineResult} traceEngineResult | ||
* @param {Map<number, number[]>} serviceWorkerThreads | ||
* @param {import('@paulirish/trace_engine/models/trace/types/TraceEvents.js').SyntheticNetworkRequest} request | ||
* @return {Lantern.NetworkRequest=} | ||
*/ | ||
static _createLanternRequest(traceEngineResult, serviceWorkerThreads, request) { | ||
if (request.args.data.connectionId === undefined || | ||
request.args.data.connectionReused === undefined) { | ||
throw new Error('Trace is too old'); | ||
} | ||
|
||
let url; | ||
try { | ||
// globalThis does exist. | ||
// eslint-disable-next-line no-undef | ||
url = new globalThis.URL(request.args.data.url); | ||
} catch (e) { | ||
continue; | ||
} | ||
let url; | ||
try { | ||
url = new URL(request.args.data.url); | ||
} catch (e) { | ||
return; | ||
} | ||
|
||
const timing = request.args.data.timing ? { | ||
...request.args.data.timing, | ||
workerFetchStart: -1, | ||
workerRespondWithSettled: -1, | ||
} : undefined; | ||
|
||
const networkRequestTime = timing ? | ||
timing.requestTime * 1000 : | ||
request.args.data.syntheticData.downloadStart / 1000; | ||
|
||
const parsedURL = { | ||
scheme: url.protocol.split(':')[0], | ||
// Intentional, DevTools uses different terminology | ||
host: url.hostname, | ||
securityOrigin: url.origin, | ||
}; | ||
|
||
let fromWorker = false; | ||
// TODO: should also check pid | ||
if (traceEngineResult.data.Workers.workerIdByThread.has(request.tid)) { | ||
fromWorker = true; | ||
} | ||
const tids = serviceWorkerThreads.get(request.pid); | ||
if (tids?.includes(request.tid)) { | ||
fromWorker = true; | ||
} | ||
|
||
// `initiator` in the trace does not contain the stack trace for JS-initiated | ||
// requests. Instead, that is stored in the `stackTrace` property of the SyntheticNetworkRequest. | ||
// There are some minor differences in the fields, accounted for here. | ||
// Most importantly, there seems to be fewer frames in the trace than the equivalent | ||
// events over the CDP. This results in less accuracy in determining the initiator request, | ||
// which means less edges in the graph, which mean worse results. Should fix. | ||
/** @type {Lantern.NetworkRequest['initiator']} */ | ||
const initiator = request.args.data.initiator ?? {type: 'other'}; | ||
if (request.args.data.stackTrace) { | ||
const callFrames = request.args.data.stackTrace.map(f => { | ||
return { | ||
scriptId: String(f.scriptId), | ||
url: f.url, | ||
lineNumber: f.lineNumber - 1, | ||
columnNumber: f.columnNumber - 1, | ||
functionName: f.functionName, | ||
}; | ||
}); | ||
initiator.stack = {callFrames}; | ||
} | ||
|
||
let resourceType = request.args.data.resourceType; | ||
if (request.args.data.initiator?.fetchType === 'xmlhttprequest') { | ||
// @ts-expect-error yes XHR is a valid ResourceType. TypeScript const enums are so unhelpful. | ||
resourceType = 'XHR'; | ||
} | ||
|
||
lanternRequests.push({ | ||
requestId: request.args.data.requestId, | ||
connectionId: request.args.data.connectionId, | ||
connectionReused: request.args.data.connectionReused, | ||
url: request.args.data.url, | ||
protocol: request.args.data.protocol, | ||
parsedURL, | ||
documentURL: request.args.data.requestingFrameUrl, | ||
rendererStartTime: request.ts / 1000, | ||
networkRequestTime, | ||
responseHeadersEndTime: request.args.data.syntheticData.downloadStart / 1000, | ||
networkEndTime: request.args.data.syntheticData.finishTime / 1000, | ||
transferSize: request.args.data.encodedDataLength, | ||
resourceSize: request.args.data.decodedBodyLength, | ||
fromDiskCache: request.args.data.syntheticData.isDiskCached, | ||
fromMemoryCache: request.args.data.syntheticData.isMemoryCached, | ||
isLinkPreload: request.args.data.isLinkPreload, | ||
finished: request.args.data.finished, | ||
failed: request.args.data.failed, | ||
statusCode: request.args.data.statusCode, | ||
initiator, | ||
timing, | ||
resourceType, | ||
mimeType: request.args.data.mimeType, | ||
priority: request.args.data.priority, | ||
frameId: request.args.data.frame, | ||
fromWorker, | ||
record: request, | ||
// Set below. | ||
redirects: undefined, | ||
redirectSource: undefined, | ||
redirectDestination: undefined, | ||
initiatorRequest: undefined, | ||
const parsedURL = { | ||
scheme: url.protocol.split(':')[0], | ||
// Intentional, DevTools uses different terminology | ||
host: url.hostname, | ||
securityOrigin: url.origin, | ||
}; | ||
|
||
const timing = request.args.data.timing ? { | ||
...request.args.data.timing, | ||
// These two timings are not included in the trace. | ||
workerFetchStart: -1, | ||
workerRespondWithSettled: -1, | ||
connorjclark marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} : undefined; | ||
|
||
const networkRequestTime = timing ? | ||
timing.requestTime * 1000 : | ||
request.args.data.syntheticData.downloadStart / 1000; | ||
|
||
let fromWorker = false; | ||
// TODO: should also check pid | ||
if (traceEngineResult.data.Workers.workerIdByThread.has(request.tid)) { | ||
fromWorker = true; | ||
} | ||
const tids = serviceWorkerThreads.get(request.pid); | ||
if (tids?.includes(request.tid)) { | ||
fromWorker = true; | ||
} | ||
|
||
// `initiator` in the trace does not contain the stack trace for JS-initiated | ||
// requests. Instead, that is stored in the `stackTrace` property of the SyntheticNetworkRequest. | ||
// There are some minor differences in the fields, accounted for here. | ||
// Most importantly, there seems to be fewer frames in the trace than the equivalent | ||
// events over the CDP. This results in less accuracy in determining the initiator request, | ||
// which means less edges in the graph, which mean worse results. Should fix. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Meaning we should add more info to the trace events? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes - this is chromium work There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we should track in b/324059246 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
i think it is known already that these call stack frames are missing data. |
||
/** @type {Lantern.NetworkRequest['initiator']} */ | ||
const initiator = request.args.data.initiator ?? {type: 'other'}; | ||
if (request.args.data.stackTrace) { | ||
const callFrames = request.args.data.stackTrace.map(f => { | ||
return { | ||
scriptId: String(f.scriptId), | ||
url: f.url, | ||
lineNumber: f.lineNumber - 1, | ||
columnNumber: f.columnNumber - 1, | ||
functionName: f.functionName, | ||
}; | ||
}); | ||
initiator.stack = {callFrames}; | ||
} | ||
|
||
let resourceType = request.args.data.resourceType; | ||
if (request.args.data.initiator?.fetchType === 'xmlhttprequest') { | ||
// @ts-expect-error yes XHR is a valid ResourceType. TypeScript const enums are so unhelpful. | ||
resourceType = 'XHR'; | ||
} | ||
|
||
return { | ||
requestId: request.args.data.requestId, | ||
connectionId: request.args.data.connectionId, | ||
connectionReused: request.args.data.connectionReused, | ||
url: request.args.data.url, | ||
protocol: request.args.data.protocol, | ||
parsedURL, | ||
documentURL: request.args.data.requestingFrameUrl, | ||
rendererStartTime: request.ts / 1000, | ||
networkRequestTime, | ||
responseHeadersEndTime: request.args.data.syntheticData.downloadStart / 1000, | ||
networkEndTime: request.args.data.syntheticData.finishTime / 1000, | ||
transferSize: request.args.data.encodedDataLength, | ||
resourceSize: request.args.data.decodedBodyLength, | ||
fromDiskCache: request.args.data.syntheticData.isDiskCached, | ||
fromMemoryCache: request.args.data.syntheticData.isMemoryCached, | ||
isLinkPreload: request.args.data.isLinkPreload, | ||
finished: request.args.data.finished, | ||
failed: request.args.data.failed, | ||
statusCode: request.args.data.statusCode, | ||
initiator, | ||
timing, | ||
resourceType, | ||
mimeType: request.args.data.mimeType, | ||
priority: request.args.data.priority, | ||
frameId: request.args.data.frame, | ||
fromWorker, | ||
record: request, | ||
// Set below. | ||
redirects: undefined, | ||
redirectSource: undefined, | ||
redirectDestination: undefined, | ||
initiatorRequest: undefined, | ||
}; | ||
} | ||
|
||
/** | ||
* | ||
* @param {Lantern.NetworkRequest[]} lanternRequests | ||
*/ | ||
static _linkInitiators(lanternRequests) { | ||
/** @type {Map<string, Lantern.NetworkRequest[]>} */ | ||
const requestsByURL = new Map(); | ||
for (const request of lanternRequests) { | ||
const requests = requestsByURL.get(request.url) || []; | ||
requests.push(request); | ||
requestsByURL.set(request.url, requests); | ||
} | ||
|
||
for (const request of lanternRequests) { | ||
const initiatorRequest = PageDependencyGraph.chooseInitiatorRequest(request, requestsByURL); | ||
if (initiatorRequest) { | ||
request.initiatorRequest = initiatorRequest; | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* @param {LH.TraceEvent[]} mainThreadEvents | ||
* @param {LH.Trace} trace | ||
* @param {LH.Artifacts.TraceEngineResult} traceEngineResult | ||
* @param {LH.Artifacts.URL} URL | ||
*/ | ||
static async createGraphFromTrace(mainThreadEvents, trace, traceEngineResult, URL) { | ||
const serviceWorkerThreads = this._findServiceWorkerThreads(trace); | ||
|
||
/** @type {Lantern.NetworkRequest[]} */ | ||
const lanternRequests = []; | ||
for (const request of traceEngineResult.data.NetworkRequests.byTime) { | ||
const lanternRequest = | ||
this._createLanternRequest(traceEngineResult, serviceWorkerThreads, request); | ||
if (lanternRequest) { | ||
lanternRequests.push(lanternRequest); | ||
} | ||
} | ||
|
||
// TraceEngine consolidates all redirects into a single request object, but lantern needs | ||
|
@@ -760,20 +804,7 @@ class PageDependencyGraph { | |
} | ||
} | ||
|
||
/** @type {Map<string, Lantern.NetworkRequest[]>} */ | ||
const requestsByURL = new Map(); | ||
for (const request of lanternRequests) { | ||
const requests = requestsByURL.get(request.url) || []; | ||
requests.push(request); | ||
requestsByURL.set(request.url, requests); | ||
} | ||
|
||
for (const request of lanternRequests) { | ||
const initiatorRequest = PageDependencyGraph.chooseInitiatorRequest(request, requestsByURL); | ||
if (initiatorRequest) { | ||
request.initiatorRequest = initiatorRequest; | ||
} | ||
} | ||
this._linkInitiators(lanternRequests); | ||
|
||
// This would already be sorted by rendererStartTime, if not for the redirect unwrapping done | ||
// above. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's possible that map already includes (same-process) serviceworkers… do you know it doesn't?
regardless having pid matching for it too seems like a requirement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch, by looking at
core/test/fixtures/traces/amp-m86.trace.json
I see that service worker threads also get the same events a worker thread does for purposes of WorkersHandlerThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I modified this method to account for all workers, so we build our own comprehensive pid/tid worker map.