@paulirish/trace_engine 0.0.51 → 0.0.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/.tmp/tsbuildinfo/tsconfig.tsbuildinfo +1 -1
  2. package/core/platform/DOMUtilities.d.ts +8 -0
  3. package/core/platform/DOMUtilities.js +14 -0
  4. package/core/platform/DOMUtilities.js.map +1 -1
  5. package/core/platform/StringUtilities.d.ts +1 -5
  6. package/core/platform/StringUtilities.js +4 -1
  7. package/core/platform/StringUtilities.js.map +1 -1
  8. package/core/platform/devtools_entrypoint-bundle-typescript-tsconfig.json +1 -1
  9. package/core/platform/platform-tsconfig.json +1 -1
  10. package/generated/protocol.d.ts +345 -44
  11. package/locales/af.json +11 -5
  12. package/locales/am.json +7 -1
  13. package/locales/ar.json +7 -1
  14. package/locales/as.json +7 -1
  15. package/locales/az.json +7 -1
  16. package/locales/be.json +7 -1
  17. package/locales/bg.json +7 -1
  18. package/locales/bn.json +7 -1
  19. package/locales/bs.json +7 -1
  20. package/locales/ca.json +7 -1
  21. package/locales/cs.json +7 -1
  22. package/locales/cy.json +7 -1
  23. package/locales/da.json +7 -1
  24. package/locales/de.json +7 -1
  25. package/locales/el.json +7 -1
  26. package/locales/en-GB.json +7 -1
  27. package/locales/en-US.json +52 -7
  28. package/locales/en-XL.json +52 -7
  29. package/locales/es-419.json +7 -1
  30. package/locales/es.json +7 -1
  31. package/locales/et.json +7 -1
  32. package/locales/eu.json +7 -1
  33. package/locales/fa.json +7 -1
  34. package/locales/fi.json +7 -1
  35. package/locales/fil.json +7 -1
  36. package/locales/fr-CA.json +7 -1
  37. package/locales/fr.json +7 -1
  38. package/locales/gl.json +7 -1
  39. package/locales/gu.json +7 -1
  40. package/locales/he.json +7 -1
  41. package/locales/hi.json +7 -1
  42. package/locales/hr.json +7 -1
  43. package/locales/hu.json +7 -1
  44. package/locales/hy.json +7 -1
  45. package/locales/id.json +7 -1
  46. package/locales/is.json +7 -1
  47. package/locales/it.json +7 -1
  48. package/locales/ja.json +7 -1
  49. package/locales/ka.json +7 -1
  50. package/locales/kk.json +7 -1
  51. package/locales/km.json +7 -1
  52. package/locales/kn.json +7 -1
  53. package/locales/ko.json +7 -1
  54. package/locales/ky.json +7 -1
  55. package/locales/lo.json +7 -1
  56. package/locales/lt.json +7 -1
  57. package/locales/lv.json +7 -1
  58. package/locales/mk.json +7 -1
  59. package/locales/ml.json +7 -1
  60. package/locales/mn.json +7 -1
  61. package/locales/mr.json +7 -1
  62. package/locales/ms.json +7 -1
  63. package/locales/my.json +7 -1
  64. package/locales/ne.json +7 -1
  65. package/locales/nl.json +7 -1
  66. package/locales/no.json +7 -1
  67. package/locales/or.json +7 -1
  68. package/locales/pa.json +7 -1
  69. package/locales/pl.json +7 -1
  70. package/locales/pt-PT.json +7 -1
  71. package/locales/pt.json +7 -1
  72. package/locales/ro.json +7 -1
  73. package/locales/ru.json +7 -1
  74. package/locales/si.json +7 -1
  75. package/locales/sk.json +7 -1
  76. package/locales/sl.json +7 -1
  77. package/locales/sq.json +7 -1
  78. package/locales/sr-Latn.json +7 -1
  79. package/locales/sr.json +7 -1
  80. package/locales/sv.json +7 -1
  81. package/locales/sw.json +7 -1
  82. package/locales/ta.json +7 -1
  83. package/locales/te.json +7 -1
  84. package/locales/th.json +7 -1
  85. package/locales/tr.json +7 -1
  86. package/locales/uk.json +7 -1
  87. package/locales/ur.json +7 -1
  88. package/locales/uz.json +7 -1
  89. package/locales/vi.json +7 -1
  90. package/locales/zh-HK.json +7 -1
  91. package/locales/zh-TW.json +7 -1
  92. package/locales/zh.json +7 -1
  93. package/locales/zu.json +7 -1
  94. package/models/cpu_profile/cpu_profile-tsconfig.json +1 -1
  95. package/models/cpu_profile/devtools_entrypoint-bundle-typescript-tsconfig.json +1 -1
  96. package/models/trace/LanternComputationData.js +3 -3
  97. package/models/trace/LanternComputationData.js.map +1 -1
  98. package/models/trace/Processor.d.ts +1 -3
  99. package/models/trace/Processor.js +1 -1
  100. package/models/trace/Processor.js.map +1 -1
  101. package/models/trace/TracingManager.js.map +1 -1
  102. package/models/trace/devtools_entrypoint-bundle-typescript-tsconfig.json +1 -1
  103. package/models/trace/extras/ScriptDuplication.d.ts +5 -2
  104. package/models/trace/extras/ScriptDuplication.js +4 -2
  105. package/models/trace/extras/ScriptDuplication.js.map +1 -1
  106. package/models/trace/extras/StackTraceForEvent.js +88 -47
  107. package/models/trace/extras/StackTraceForEvent.js.map +1 -1
  108. package/models/trace/extras/ThirdParties.d.ts +13 -12
  109. package/models/trace/extras/ThirdParties.js +50 -19
  110. package/models/trace/extras/ThirdParties.js.map +1 -1
  111. package/models/trace/extras/devtools_entrypoint-bundle-typescript-tsconfig.json +1 -1
  112. package/models/trace/extras/extras-tsconfig.json +1 -1
  113. package/models/trace/extras/extras.d.ts +98 -6
  114. package/models/trace/extras/extras.js +98 -6
  115. package/models/trace/handlers/AsyncJSCallsHandler.d.ts +5 -0
  116. package/models/trace/handlers/AsyncJSCallsHandler.js +12 -9
  117. package/models/trace/handlers/AsyncJSCallsHandler.js.map +1 -1
  118. package/models/trace/handlers/FramesHandler.js.map +1 -1
  119. package/models/trace/handlers/NetworkRequestsHandler.d.ts +1 -0
  120. package/models/trace/handlers/NetworkRequestsHandler.js +37 -23
  121. package/models/trace/handlers/NetworkRequestsHandler.js.map +1 -1
  122. package/models/trace/handlers/ScriptsHandler.d.ts +5 -1
  123. package/models/trace/handlers/ScriptsHandler.js +59 -22
  124. package/models/trace/handlers/ScriptsHandler.js.map +1 -1
  125. package/models/trace/handlers/WarningsHandler.js +14 -2
  126. package/models/trace/handlers/WarningsHandler.js.map +1 -1
  127. package/models/trace/handlers/devtools_entrypoint-bundle-typescript-tsconfig.json +1 -1
  128. package/models/trace/handlers/handlers-tsconfig.json +1 -1
  129. package/models/trace/handlers/helpers.js +12 -0
  130. package/models/trace/handlers/helpers.js.map +1 -1
  131. package/models/trace/handlers/types.d.ts +2 -6
  132. package/models/trace/handlers/types.js.map +1 -1
  133. package/models/trace/helpers/devtools_entrypoint-bundle-typescript-tsconfig.json +1 -1
  134. package/models/trace/helpers/helpers-tsconfig.json +1 -1
  135. package/models/trace/insights/CLSCulprits.d.ts +6 -1
  136. package/models/trace/insights/CLSCulprits.js +11 -2
  137. package/models/trace/insights/CLSCulprits.js.map +1 -1
  138. package/models/trace/insights/Cache.d.ts +0 -1
  139. package/models/trace/insights/Cache.js +8 -7
  140. package/models/trace/insights/Cache.js.map +1 -1
  141. package/models/trace/insights/Common.js +6 -0
  142. package/models/trace/insights/Common.js.map +1 -1
  143. package/models/trace/insights/DocumentLatency.d.ts +7 -3
  144. package/models/trace/insights/DocumentLatency.js +25 -8
  145. package/models/trace/insights/DocumentLatency.js.map +1 -1
  146. package/models/trace/insights/DuplicatedJavaScript.js +9 -3
  147. package/models/trace/insights/DuplicatedJavaScript.js.map +1 -1
  148. package/models/trace/insights/ImageDelivery.d.ts +0 -1
  149. package/models/trace/insights/ImageDelivery.js +1 -1
  150. package/models/trace/insights/ImageDelivery.js.map +1 -1
  151. package/models/trace/insights/LCPPhases.d.ts +1 -1
  152. package/models/trace/insights/LCPPhases.js +1 -1
  153. package/models/trace/insights/LCPPhases.js.map +1 -1
  154. package/models/trace/insights/LegacyJavaScript.d.ts +1 -1
  155. package/models/trace/insights/LegacyJavaScript.js +6 -3
  156. package/models/trace/insights/LegacyJavaScript.js.map +1 -1
  157. package/models/trace/insights/NetworkDependencyTree.d.ts +151 -3
  158. package/models/trace/insights/NetworkDependencyTree.js +648 -10
  159. package/models/trace/insights/NetworkDependencyTree.js.map +1 -1
  160. package/models/trace/insights/RenderBlocking.js +1 -1
  161. package/models/trace/insights/RenderBlocking.js.map +1 -1
  162. package/models/trace/insights/ThirdParties.d.ts +1 -1
  163. package/models/trace/insights/ThirdParties.js +7 -7
  164. package/models/trace/insights/ThirdParties.js.map +1 -1
  165. package/models/trace/insights/devtools_entrypoint-bundle-typescript-tsconfig.json +1 -1
  166. package/models/trace/insights/insights-tsconfig.json +1 -1
  167. package/models/trace/insights/types.d.ts +11 -0
  168. package/models/trace/insights/types.js.map +1 -1
  169. package/models/trace/lantern/core/core-tsconfig.json +1 -1
  170. package/models/trace/lantern/core/devtools_entrypoint-bundle-typescript-tsconfig.json +1 -1
  171. package/models/trace/lantern/devtools_entrypoint-bundle-typescript-tsconfig.json +1 -1
  172. package/models/trace/lantern/graph/devtools_entrypoint-bundle-typescript-tsconfig.json +1 -1
  173. package/models/trace/lantern/graph/graph-tsconfig.json +1 -1
  174. package/models/trace/lantern/lantern-tsconfig.json +1 -1
  175. package/models/trace/lantern/metrics/devtools_entrypoint-bundle-typescript-tsconfig.json +1 -1
  176. package/models/trace/lantern/metrics/metrics-tsconfig.json +1 -1
  177. package/models/trace/lantern/simulation/devtools_entrypoint-bundle-typescript-tsconfig.json +1 -1
  178. package/models/trace/lantern/simulation/simulation-tsconfig.json +1 -1
  179. package/models/trace/lantern/types/Lantern.d.ts +4 -7
  180. package/models/trace/lantern/types/Lantern.js +1 -0
  181. package/models/trace/lantern/types/Lantern.js.map +1 -1
  182. package/models/trace/lantern/types/devtools_entrypoint-bundle-typescript-tsconfig.json +1 -1
  183. package/models/trace/lantern/types/types-tsconfig.json +1 -1
  184. package/models/trace/trace-tsconfig.json +1 -1
  185. package/models/trace/types/Configuration.d.ts +2 -0
  186. package/models/trace/types/Configuration.js.map +1 -1
  187. package/models/trace/types/Extensions.d.ts +1 -3
  188. package/models/trace/types/Extensions.js.map +1 -1
  189. package/models/trace/types/File.d.ts +2 -1
  190. package/models/trace/types/File.js.map +1 -1
  191. package/models/trace/types/TraceEvents.d.ts +51 -26
  192. package/models/trace/types/TraceEvents.js +6 -0
  193. package/models/trace/types/TraceEvents.js.map +1 -1
  194. package/models/trace/types/devtools_entrypoint-bundle-typescript-tsconfig.json +1 -1
  195. package/models/trace/types/types-tsconfig.json +1 -1
  196. package/package.json +1 -1
  197. package/test/test-trace-engine.mjs +2 -2
@@ -2,6 +2,7 @@
2
2
  // Use of this source code is governed by a BSD-style license that can be
3
3
  // found in the LICENSE file.
4
4
  // import * as i18n from '../../../core/i18n/i18n.js';
5
+ import * as Platform from '../../../core/platform/platform.js';
5
6
  import * as Helpers from '../helpers/helpers.js';
6
7
  import * as Types from '../types/types.js';
7
8
  import { InsightCategory, } from './types.js';
@@ -26,7 +27,55 @@ export const UIStrings = {
26
27
  * @description Text for the maximum critical path latency. This refers to the longest chain of network requests that
27
28
  * the browser must download before it can render the page.
28
29
  */
29
- maxCriticalPathLatency: 'Max critical path latency:'
30
+ maxCriticalPathLatency: 'Max critical path latency:',
31
+ /** Label for a column in a data table; entries will be the network request */
32
+ columnRequest: 'Request',
33
+ /** Label for a column in a data table; entries will be the time from main document till current network request. */
34
+ columnTime: 'Time',
35
+ /**
36
+ * @description Title of the table of the detected preconnect origins.
37
+ */
38
+ preconnectOriginsTableTitle: 'Preconnect origins',
39
+ /**
40
+ * @description Description of the table of the detected preconnect origins.
41
+ */
42
+ preconnectOriginsTableDescription: '[preconnect](https://developer.chrome.com/docs/lighthouse/performance/uses-rel-preconnect/) hints help the browser establish a connection earlier in the page load, saving time when the first request for that origin is made. The following are the origins that the page preconnected to.',
43
+ /**
44
+ * @description Text status indicating that there isn't any preconnected origins.
45
+ */
46
+ noPreconnectOrigins: 'no origins were preconnected',
47
+ /**
48
+ * @description A warning message that is shown when found more than 4 preconnected links
49
+ */
50
+ tooManyPreconnectLinksWarning: 'More than 4 `preconnect` connections were found. These should be used sparingly and only to the most important origins.',
51
+ /**
52
+ * @description A warning message that is shown when the user added preconnect for some unnecessary origins.
53
+ */
54
+ unusedWarning: 'Unused preconnect. Only use `preconnect` for origins that the page is likely to request.',
55
+ /**
56
+ * @description Label for a column in a data table; entries will be the source of the origin.
57
+ */
58
+ columnSource: 'Source',
59
+ /**
60
+ * @description Text status indicating that there isn't preconnect candidates.
61
+ */
62
+ noPreconnectCandidates: 'No additional origins are good candidates for preconnecting',
63
+ /**
64
+ * @description Title of the table that shows the origins that the page should have preconnected to.
65
+ */
66
+ estSavingTableTitle: 'Preconnect candidates',
67
+ /**
68
+ * @description Description of the table that recommends preconnecting to the origins to save time.
69
+ */
70
+ estSavingTableDescription: 'Add [preconnect](https://developer.chrome.com/docs/lighthouse/performance/uses-rel-preconnect/) hints to your most important origins, but try to use fewer than 4.',
71
+ /**
72
+ * @description Label for a column in a data table; entries will be the origin of a web resource
73
+ */
74
+ columnOrigin: 'Origin',
75
+ /**
76
+ * @description Label for a column in a data table; entries will be the number of milliseconds the user could reduce page load by if they implemented the suggestions.
77
+ */
78
+ columnWastedMs: 'Est LCP savings',
30
79
  };
31
80
  // const str_ = i18n.i18n.registerUIStrings('models/trace/insights/NetworkDependencyTree.ts', UIStrings);
32
81
  export const i18nString = (i18nId, values) => ({i18nId, values}); // i18n.i18n.getLocalizedString.bind(undefined, str_);
@@ -38,6 +87,13 @@ const nonCriticalResourceTypes = new Set([
38
87
  "Fetch" /* Protocol.Network.ResourceType.Fetch */,
39
88
  "EventSource" /* Protocol.Network.ResourceType.EventSource */,
40
89
  ]);
90
+ // Preconnect establishes a "clean" socket. Chrome's socket manager will keep an unused socket
91
+ // around for 10s. Meaning, the time delta between processing preconnect a request should be <10s,
92
+ // otherwise it's wasted. We add a 5s margin so we are sure to capture all key requests.
93
+ // @see https://github.com/GoogleChrome/lighthouse/issues/3106#issuecomment-333653747
94
+ const PRECONNECT_SOCKET_MAX_IDLE_IN_MS = Types.Timing.Milli(15_000);
95
+ const IGNORE_THRESHOLD_IN_MILLISECONDS = Types.Timing.Milli(50);
96
+ export const TOO_MANY_PRECONNECTS_THRESHOLD = 4;
41
97
  function finalize(partialModel) {
42
98
  return {
43
99
  insightKey: "NetworkDependencyTree" /* InsightKeys.NETWORK_DEPENDENCY_TREE */,
@@ -76,14 +132,7 @@ function isCritical(request, context) {
76
132
  const isHighPriority = Helpers.Network.isSyntheticNetworkRequestHighPriority(request);
77
133
  return isHighPriority || isBlocking;
78
134
  }
79
- export function generateInsight(_parsedTrace, context) {
80
- if (!context.navigation) {
81
- return finalize({
82
- rootNodes: [],
83
- maxTime: Types.Timing.Micro(0),
84
- fail: false,
85
- });
86
- }
135
+ function generateNetworkDependencyTree(context) {
87
136
  const rootNodes = [];
88
137
  const relatedEvents = new Map();
89
138
  let maxTime = Types.Timing.Micro(0);
@@ -119,7 +168,8 @@ export function generateInsight(_parsedTrace, context) {
119
168
  currentNodes.push(found);
120
169
  }
121
170
  path.forEach(request => found?.relatedRequests.add(request));
122
- // TODO(b/372897712) Switch the UIString to markdown.
171
+ // TODO(b/372897712): When RelatedInsight supports markdown, remove
172
+ // UIStrings.warningDescription and use UIStrings.description.
123
173
  relatedEvents.set(request, depth < 2 ? [] : [i18nString(UIStrings.warningDescription)]);
124
174
  currentNodes = found.children;
125
175
  }
@@ -165,11 +215,599 @@ export function generateInsight(_parsedTrace, context) {
165
215
  }
166
216
  }
167
217
  }
218
+ return {
219
+ rootNodes,
220
+ maxTime,
221
+ fail,
222
+ relatedEvents,
223
+ };
224
+ }
225
+ function getSecurityOrigin(url) {
226
+ const parsedURL = new ParsedURL(url);
227
+ return parsedURL.securityOrigin();
228
+ }
229
+ // Export the function for test purpose.
230
+ export function generatePreconnectedOrigins(linkPreconnectEvents, contextRequests) {
231
+ const preconnectOrigins = [];
232
+ for (const event of linkPreconnectEvents) {
233
+ preconnectOrigins.push({
234
+ node_id: event.args.data.node_id,
235
+ frame: event.args.data.frame,
236
+ url: event.args.data.url,
237
+ unused: !contextRequests.some(request => getSecurityOrigin(event.args.data.url) === getSecurityOrigin(request.args.data.url)),
238
+ });
239
+ }
240
+ return preconnectOrigins;
241
+ }
242
+ function hasValidTiming(request) {
243
+ return !!request.args.data.timing && request.args.data.timing.connectEnd >= 0 &&
244
+ request.args.data.timing.connectStart >= 0;
245
+ }
246
+ function hasAlreadyConnectedToOrigin(request) {
247
+ const { timing } = request.args.data;
248
+ if (!timing) {
249
+ return false;
250
+ }
251
+ // When these values are given as -1, that means the page has
252
+ // a connection for this origin and paid these costs already.
253
+ if (timing.dnsStart === -1 && timing.dnsEnd === -1 && timing.connectStart === -1 && timing.connectEnd === -1) {
254
+ return true;
255
+ }
256
+ // Less understood: if the connection setup took no time at all, consider
257
+ // it the same as the above. It is unclear if this is correct, or is even possible.
258
+ if (timing.dnsEnd - timing.dnsStart === 0 && timing.connectEnd - timing.connectStart === 0) {
259
+ return true;
260
+ }
261
+ return false;
262
+ }
263
+ function socketStartTimeIsBelowThreshold(request, mainResource) {
264
+ const timeSinceMainEnd = Math.max(0, request.args.data.syntheticData.sendStartTime - mainResource.args.data.syntheticData.finishTime);
265
+ return Helpers.Timing.microToMilli(timeSinceMainEnd) < PRECONNECT_SOCKET_MAX_IDLE_IN_MS;
266
+ }
267
+ function candidateRequestsByOrigin(parsedTrace, mainResource, contextRequests, lcpGraphURLs) {
268
+ const origins = new Map();
269
+ contextRequests.forEach(request => {
270
+ if (!hasValidTiming(request)) {
271
+ return;
272
+ }
273
+ // Filter out all resources that are loaded by the document. Connections are already early.
274
+ if (parsedTrace.NetworkRequests.eventToInitiator.get(request) === mainResource) {
275
+ return;
276
+ }
277
+ const url = new URL(request.args.data.url);
278
+ // Filter out urls that do not have an origin (data, file, etc).
279
+ if (url.origin === 'null') {
280
+ return;
281
+ }
282
+ const mainOrigin = new URL(mainResource.args.data.url).origin;
283
+ // Filter out all resources that have the same origin. We're already connected.
284
+ if (url.origin === mainOrigin) {
285
+ return;
286
+ }
287
+ // Filter out anything that wasn't part of LCP. Only recommend important origins.
288
+ if (!lcpGraphURLs.has(request.args.data.url)) {
289
+ return;
290
+ }
291
+ // Filter out all resources where origins are already resolved.
292
+ if (hasAlreadyConnectedToOrigin(request)) {
293
+ return;
294
+ }
295
+ // Make sure the requests are below the PRECONNECT_SOCKET_MAX_IDLE_IN_MS (15s) mark.
296
+ if (!socketStartTimeIsBelowThreshold(request, mainResource)) {
297
+ return;
298
+ }
299
+ const originRequests = Platform.MapUtilities.getWithDefault(origins, url.origin, () => []);
300
+ originRequests.push(request);
301
+ });
302
+ return origins;
303
+ }
304
+ // Export the function for test purpose.
305
+ export function generatePreconnectCandidates(parsedTrace, context, contextRequests) {
306
+ if (!context.lantern) {
307
+ return [];
308
+ }
309
+ const mainResource = contextRequests.find(request => request.args.data.requestId === context.navigationId);
310
+ if (!mainResource) {
311
+ return [];
312
+ }
313
+ const { rtt, additionalRttByOrigin } = context.lantern.simulator.getOptions();
314
+ const lcpGraph = context.lantern.metrics.largestContentfulPaint.pessimisticGraph;
315
+ const fcpGraph = context.lantern.metrics.firstContentfulPaint.pessimisticGraph;
316
+ const lcpGraphURLs = new Set();
317
+ lcpGraph.traverse(node => {
318
+ if (node.type === 'network') {
319
+ lcpGraphURLs.add(node.request.url);
320
+ }
321
+ });
322
+ const fcpGraphURLs = new Set();
323
+ fcpGraph.traverse(node => {
324
+ if (node.type === 'network') {
325
+ fcpGraphURLs.add(node.request.url);
326
+ }
327
+ });
328
+ const origins = candidateRequestsByOrigin(parsedTrace, mainResource, contextRequests, lcpGraphURLs);
329
+ let maxWastedLcp = Types.Timing.Milli(0);
330
+ let maxWastedFcp = Types.Timing.Milli(0);
331
+ let preconnectCandidates = [];
332
+ origins.forEach(requests => {
333
+ const firstRequestOfOrigin = requests[0];
334
+ // Skip the origin if we don't have timing information
335
+ if (!firstRequestOfOrigin.args.data.timing) {
336
+ return;
337
+ }
338
+ const firstRequestOfOriginParsedURL = new ParsedURL(firstRequestOfOrigin.args.data.url);
339
+ const origin = firstRequestOfOriginParsedURL.securityOrigin();
340
+ // Approximate the connection time with the duration of TCP (+potentially SSL) handshake
341
+ // DNS time can be large but can also be 0 if a commonly used origin that's cached, so make
342
+ // no assumption about DNS.
343
+ const additionalRtt = additionalRttByOrigin.get(origin) ?? 0;
344
+ let connectionTime = Types.Timing.Milli(rtt + additionalRtt);
345
+ // TCP Handshake will be at least 2 RTTs for TLS connections
346
+ if (firstRequestOfOriginParsedURL.scheme === 'https') {
347
+ connectionTime = Types.Timing.Milli(connectionTime * 2);
348
+ }
349
+ const timeBetweenMainResourceAndDnsStart = Types.Timing.Micro(firstRequestOfOrigin.args.data.syntheticData.sendStartTime - mainResource.args.data.syntheticData.finishTime +
350
+ Helpers.Timing.milliToMicro(firstRequestOfOrigin.args.data.timing.dnsStart));
351
+ const wastedMs = Math.min(connectionTime, Helpers.Timing.microToMilli(timeBetweenMainResourceAndDnsStart));
352
+ if (wastedMs < IGNORE_THRESHOLD_IN_MILLISECONDS) {
353
+ return;
354
+ }
355
+ maxWastedLcp = Math.max(wastedMs, maxWastedLcp);
356
+ if (fcpGraphURLs.has(firstRequestOfOrigin.args.data.url)) {
357
+ maxWastedFcp = Math.max(wastedMs, maxWastedFcp);
358
+ }
359
+ preconnectCandidates.push({
360
+ origin,
361
+ wastedMs,
362
+ });
363
+ });
364
+ preconnectCandidates = preconnectCandidates.sort((a, b) => b.wastedMs - a.wastedMs);
365
+ return preconnectCandidates.slice(0, TOO_MANY_PRECONNECTS_THRESHOLD);
366
+ }
367
+ export function generateInsight(parsedTrace, context) {
368
+ if (!context.navigation) {
369
+ return finalize({
370
+ rootNodes: [],
371
+ maxTime: 0,
372
+ fail: false,
373
+ preconnectOrigins: [],
374
+ preconnectCandidates: [],
375
+ });
376
+ }
377
+ const { rootNodes, maxTime, fail, relatedEvents, } = generateNetworkDependencyTree(context);
378
+ const isWithinContext = (event) => Helpers.Timing.eventIsInBounds(event, context.bounds);
379
+ const contextRequests = parsedTrace.NetworkRequests.byTime.filter(isWithinContext);
380
+ const preconnectOrigins = generatePreconnectedOrigins(parsedTrace.NetworkRequests.linkPreconnectEvents, contextRequests);
381
+ const preconnectCandidates = generatePreconnectCandidates(parsedTrace, context, contextRequests);
168
382
  return finalize({
169
383
  rootNodes,
170
384
  maxTime,
171
385
  fail,
172
386
  relatedEvents,
387
+ preconnectOrigins,
388
+ preconnectCandidates,
173
389
  });
174
390
  }
391
+ // the rest of this file is copied from core/common/common.js, which can't be bundled right now.
392
+ /**
393
+ * http://tools.ietf.org/html/rfc3986#section-5.2.4
394
+ */
395
+ export function normalizePath(path) {
396
+ if (path.indexOf('..') === -1 && path.indexOf('.') === -1) {
397
+ return path;
398
+ }
399
+ // Remove leading slash (will be added back below) so we
400
+ // can handle all (including empty) segments consistently.
401
+ const segments = (path[0] === '/' ? path.substring(1) : path).split('/');
402
+ const normalizedSegments = [];
403
+ for (const segment of segments) {
404
+ if (segment === '.') {
405
+ continue;
406
+ }
407
+ else if (segment === '..') {
408
+ normalizedSegments.pop();
409
+ }
410
+ else {
411
+ normalizedSegments.push(segment);
412
+ }
413
+ }
414
+ let normalizedPath = normalizedSegments.join('/');
415
+ if (path[0] === '/' && normalizedPath) {
416
+ normalizedPath = '/' + normalizedPath;
417
+ }
418
+ if (normalizedPath[normalizedPath.length - 1] !== '/' &&
419
+ ((path[path.length - 1] === '/') || (segments[segments.length - 1] === '.') ||
420
+ (segments[segments.length - 1] === '..'))) {
421
+ normalizedPath = normalizedPath + '/';
422
+ }
423
+ return normalizedPath;
424
+ }
425
+ export function schemeIs(url, scheme) {
426
+ try {
427
+ return (new URL(url)).protocol === scheme;
428
+ }
429
+ catch {
430
+ return false;
431
+ }
432
+ }
433
+ export class ParsedURL {
434
+ isValid;
435
+ url;
436
+ scheme;
437
+ user;
438
+ host;
439
+ port;
440
+ path;
441
+ queryParams;
442
+ fragment;
443
+ folderPathComponents;
444
+ lastPathComponent;
445
+ blobInnerScheme;
446
+ constructor(url) {
447
+ this.isValid = false;
448
+ this.url = url;
449
+ this.scheme = '';
450
+ this.user = '';
451
+ this.host = '';
452
+ this.port = '';
453
+ this.path = '';
454
+ this.queryParams = '';
455
+ this.fragment = '';
456
+ this.folderPathComponents = '';
457
+ this.lastPathComponent = '';
458
+ const isBlobUrl = this.url.startsWith('blob:');
459
+ const urlToMatch = isBlobUrl ? url.substring(5) : url;
460
+ const match = urlToMatch.match(ParsedURL.urlRegex());
461
+ if (match) {
462
+ this.isValid = true;
463
+ if (isBlobUrl) {
464
+ this.blobInnerScheme = match[2].toLowerCase();
465
+ this.scheme = 'blob';
466
+ }
467
+ else {
468
+ this.scheme = match[2].toLowerCase();
469
+ }
470
+ this.user = match[3] ?? '';
471
+ this.host = match[4] ?? '';
472
+ this.port = match[5] ?? '';
473
+ this.path = match[6] ?? '/';
474
+ this.queryParams = match[7] ?? '';
475
+ this.fragment = match[8] ?? '';
476
+ }
477
+ else {
478
+ if (this.url.startsWith('data:')) {
479
+ this.scheme = 'data';
480
+ return;
481
+ }
482
+ if (this.url.startsWith('blob:')) {
483
+ this.scheme = 'blob';
484
+ return;
485
+ }
486
+ if (this.url === 'about:blank') {
487
+ this.scheme = 'about';
488
+ return;
489
+ }
490
+ this.path = this.url;
491
+ }
492
+ const lastSlashExceptTrailingIndex = this.path.lastIndexOf('/', this.path.length - 2);
493
+ if (lastSlashExceptTrailingIndex !== -1) {
494
+ this.lastPathComponent = this.path.substring(lastSlashExceptTrailingIndex + 1);
495
+ }
496
+ else {
497
+ this.lastPathComponent = this.path;
498
+ }
499
+ const lastSlashIndex = this.path.lastIndexOf('/');
500
+ if (lastSlashIndex !== -1) {
501
+ this.folderPathComponents = this.path.substring(0, lastSlashIndex);
502
+ }
503
+ }
504
+ static fromString(string) {
505
+ const parsedURL = new ParsedURL(string.toString());
506
+ if (parsedURL.isValid) {
507
+ return parsedURL;
508
+ }
509
+ return null;
510
+ }
511
+ static preEncodeSpecialCharactersInPath(path) {
512
+ // Based on net::FilePathToFileURL. Ideally we would handle
513
+ // '\\' as well on non-Windows file systems.
514
+ for (const specialChar of ['%', ';', '#', '?', ' ']) {
515
+ (path) = path.replaceAll(specialChar, encodeURIComponent(specialChar));
516
+ }
517
+ return path;
518
+ }
519
+ static rawPathToEncodedPathString(path) {
520
+ const partiallyEncoded = ParsedURL.preEncodeSpecialCharactersInPath(path);
521
+ if (path.startsWith('/')) {
522
+ return new URL(partiallyEncoded, 'file:///').pathname;
523
+ }
524
+ // URL prepends a '/'
525
+ return new URL('/' + partiallyEncoded, 'file:///').pathname.substr(1);
526
+ }
527
+ /**
528
+ * @param name Must not be encoded
529
+ */
530
+ static encodedFromParentPathAndName(parentPath, name) {
531
+ return ParsedURL.concatenate(parentPath, '/', ParsedURL.preEncodeSpecialCharactersInPath(name));
532
+ }
533
+ /**
534
+ * @param name Must not be encoded
535
+ */
536
+ static urlFromParentUrlAndName(parentUrl, name) {
537
+ return ParsedURL.concatenate(parentUrl, '/', ParsedURL.preEncodeSpecialCharactersInPath(name));
538
+ }
539
+ static encodedPathToRawPathString(encPath) {
540
+ return decodeURIComponent(encPath);
541
+ }
542
+ static rawPathToUrlString(fileSystemPath) {
543
+ let preEncodedPath = ParsedURL.preEncodeSpecialCharactersInPath(fileSystemPath.replace(/\\/g, '/'));
544
+ preEncodedPath = preEncodedPath.replace(/\\/g, '/');
545
+ if (!preEncodedPath.startsWith('file://')) {
546
+ if (preEncodedPath.startsWith('/')) {
547
+ preEncodedPath = 'file://' + preEncodedPath;
548
+ }
549
+ else {
550
+ preEncodedPath = 'file:///' + preEncodedPath;
551
+ }
552
+ }
553
+ return new URL(preEncodedPath).toString();
554
+ }
555
+ static relativePathToUrlString(relativePath, baseURL) {
556
+ const preEncodedPath = ParsedURL.preEncodeSpecialCharactersInPath(relativePath.replace(/\\/g, '/'));
557
+ return new URL(preEncodedPath, baseURL).toString();
558
+ }
559
+ static urlToRawPathString(fileURL, isWindows) {
560
+ console.assert(fileURL.startsWith('file://'), 'This must be a file URL.');
561
+ const decodedFileURL = decodeURIComponent(fileURL);
562
+ if (isWindows) {
563
+ return decodedFileURL.substr('file:///'.length).replace(/\//g, '\\');
564
+ }
565
+ return decodedFileURL.substr('file://'.length);
566
+ }
567
+ static sliceUrlToEncodedPathString(url, start) {
568
+ return url.substring(start);
569
+ }
570
+ static substr(devToolsPath, from, length) {
571
+ return devToolsPath.substr(from, length);
572
+ }
573
+ static substring(devToolsPath, start, end) {
574
+ return devToolsPath.substring(start, end);
575
+ }
576
+ static prepend(prefix, devToolsPath) {
577
+ return prefix + devToolsPath;
578
+ }
579
+ static concatenate(devToolsPath, ...appendage) {
580
+ return devToolsPath.concat(...appendage);
581
+ }
582
+ static trim(devToolsPath) {
583
+ return devToolsPath.trim();
584
+ }
585
+ static slice(devToolsPath, start, end) {
586
+ return devToolsPath.slice(start, end);
587
+ }
588
+ static join(devToolsPaths, separator) {
589
+ return devToolsPaths.join(separator);
590
+ }
591
+ static split(devToolsPath, separator, limit) {
592
+ return devToolsPath.split(separator, limit);
593
+ }
594
+ static toLowerCase(devToolsPath) {
595
+ return devToolsPath.toLowerCase();
596
+ }
597
+ static isValidUrlString(str) {
598
+ return new ParsedURL(str).isValid;
599
+ }
600
+ static urlWithoutHash(url) {
601
+ const hashIndex = url.indexOf('#');
602
+ if (hashIndex !== -1) {
603
+ return url.substr(0, hashIndex);
604
+ }
605
+ return url;
606
+ }
607
+ static urlRegex() {
608
+ if (ParsedURL.urlRegexInstance) {
609
+ return ParsedURL.urlRegexInstance;
610
+ }
611
+ // RegExp groups:
612
+ // 1 - scheme, hostname, ?port
613
+ // 2 - scheme (using the RFC3986 grammar)
614
+ // 3 - ?user:password
615
+ // 4 - hostname
616
+ // 5 - ?port
617
+ // 6 - ?path
618
+ // 7 - ?query
619
+ // 8 - ?fragment
620
+ const schemeRegex = /([A-Za-z][A-Za-z0-9+.-]*):\/\//;
621
+ const userRegex = /(?:([A-Za-z0-9\-._~%!$&'()*+,;=:]*)@)?/;
622
+ const hostRegex = /((?:\[::\d?\])|(?:[^\s\/:]*))/;
623
+ const portRegex = /(?::([\d]+))?/;
624
+ const pathRegex = /(\/[^#?]*)?/;
625
+ const queryRegex = /(?:\?([^#]*))?/;
626
+ const fragmentRegex = /(?:#(.*))?/;
627
+ ParsedURL.urlRegexInstance = new RegExp('^(' + schemeRegex.source + userRegex.source + hostRegex.source + portRegex.source + ')' + pathRegex.source +
628
+ queryRegex.source + fragmentRegex.source + '$');
629
+ return ParsedURL.urlRegexInstance;
630
+ }
631
+ static extractPath(url) {
632
+ const parsedURL = this.fromString(url);
633
+ return (parsedURL ? parsedURL.path : '');
634
+ }
635
+ static extractOrigin(url) {
636
+ const parsedURL = this.fromString(url);
637
+ return parsedURL ? parsedURL.securityOrigin() : '';
638
+ }
639
+ static extractExtension(url) {
640
+ url = ParsedURL.urlWithoutHash(url);
641
+ const indexOfQuestionMark = url.indexOf('?');
642
+ if (indexOfQuestionMark !== -1) {
643
+ url = url.substr(0, indexOfQuestionMark);
644
+ }
645
+ const lastIndexOfSlash = url.lastIndexOf('/');
646
+ if (lastIndexOfSlash !== -1) {
647
+ url = url.substr(lastIndexOfSlash + 1);
648
+ }
649
+ const lastIndexOfDot = url.lastIndexOf('.');
650
+ if (lastIndexOfDot !== -1) {
651
+ url = url.substr(lastIndexOfDot + 1);
652
+ const lastIndexOfPercent = url.indexOf('%');
653
+ if (lastIndexOfPercent !== -1) {
654
+ return url.substr(0, lastIndexOfPercent);
655
+ }
656
+ return url;
657
+ }
658
+ return '';
659
+ }
660
+ static extractName(url) {
661
+ let index = url.lastIndexOf('/');
662
+ const pathAndQuery = index !== -1 ? url.substr(index + 1) : url;
663
+ index = pathAndQuery.indexOf('?');
664
+ return index < 0 ? pathAndQuery : pathAndQuery.substr(0, index);
665
+ }
666
+ static completeURL(baseURL, href) {
667
+ // Return special URLs as-is.
668
+ if (href.startsWith('data:') || href.startsWith('blob:') || href.startsWith('javascript:') ||
669
+ href.startsWith('mailto:')) {
670
+ return href;
671
+ }
672
+ // Return absolute URLs with normalized path and other components as-is.
673
+ const trimmedHref = href.trim();
674
+ const parsedHref = this.fromString(trimmedHref);
675
+ if (parsedHref?.scheme) {
676
+ const securityOrigin = parsedHref.securityOrigin();
677
+ const pathText = normalizePath(parsedHref.path);
678
+ const queryText = parsedHref.queryParams && `?${parsedHref.queryParams}`;
679
+ const fragmentText = parsedHref.fragment && `#${parsedHref.fragment}`;
680
+ return securityOrigin + pathText + queryText + fragmentText;
681
+ }
682
+ const parsedURL = this.fromString(baseURL);
683
+ if (!parsedURL) {
684
+ return null;
685
+ }
686
+ if (parsedURL.isDataURL()) {
687
+ return href;
688
+ }
689
+ if (href.length > 1 && href.charAt(0) === '/' && href.charAt(1) === '/') {
690
+ // href starts with "//" which is a full URL with the protocol dropped (use the baseURL protocol).
691
+ return parsedURL.scheme + ':' + href;
692
+ }
693
+ const securityOrigin = parsedURL.securityOrigin();
694
+ const pathText = parsedURL.path;
695
+ const queryText = parsedURL.queryParams ? '?' + parsedURL.queryParams : '';
696
+ // Empty href resolves to a URL without fragment.
697
+ if (!href.length) {
698
+ return securityOrigin + pathText + queryText;
699
+ }
700
+ if (href.charAt(0) === '#') {
701
+ return securityOrigin + pathText + queryText + href;
702
+ }
703
+ if (href.charAt(0) === '?') {
704
+ return securityOrigin + pathText + href;
705
+ }
706
+ const hrefMatches = href.match(/^[^#?]*/);
707
+ if (!hrefMatches || !href.length) {
708
+ throw new Error('Invalid href');
709
+ }
710
+ let hrefPath = hrefMatches[0];
711
+ const hrefSuffix = href.substring(hrefPath.length);
712
+ if (hrefPath.charAt(0) !== '/') {
713
+ hrefPath = parsedURL.folderPathComponents + '/' + hrefPath;
714
+ }
715
+ return securityOrigin + normalizePath(hrefPath) + hrefSuffix;
716
+ }
717
+ static splitLineAndColumn(string) {
718
+ // Only look for line and column numbers in the path to avoid matching port numbers.
719
+ const beforePathMatch = string.match(ParsedURL.urlRegex());
720
+ let beforePath = '';
721
+ let pathAndAfter = string;
722
+ if (beforePathMatch) {
723
+ beforePath = beforePathMatch[1];
724
+ pathAndAfter = string.substring(beforePathMatch[1].length);
725
+ }
726
+ const lineColumnRegEx = /(?::(\d+))?(?::(\d+))?$/;
727
+ const lineColumnMatch = lineColumnRegEx.exec(pathAndAfter);
728
+ let lineNumber;
729
+ let columnNumber;
730
+ console.assert(Boolean(lineColumnMatch));
731
+ if (!lineColumnMatch) {
732
+ return { url: string, lineNumber: 0, columnNumber: 0 };
733
+ }
734
+ if (typeof (lineColumnMatch[1]) === 'string') {
735
+ lineNumber = parseInt(lineColumnMatch[1], 10);
736
+ // Immediately convert line and column to 0-based numbers.
737
+ lineNumber = isNaN(lineNumber) ? undefined : lineNumber - 1;
738
+ }
739
+ if (typeof (lineColumnMatch[2]) === 'string') {
740
+ columnNumber = parseInt(lineColumnMatch[2], 10);
741
+ columnNumber = isNaN(columnNumber) ? undefined : columnNumber - 1;
742
+ }
743
+ let url = beforePath + pathAndAfter.substring(0, pathAndAfter.length - lineColumnMatch[0].length);
744
+ if (lineColumnMatch[1] === undefined && lineColumnMatch[2] === undefined) {
745
+ const wasmCodeOffsetRegex = /wasm-function\[\d+\]:0x([a-z0-9]+)$/g;
746
+ const wasmCodeOffsetMatch = wasmCodeOffsetRegex.exec(pathAndAfter);
747
+ if (wasmCodeOffsetMatch && typeof (wasmCodeOffsetMatch[1]) === 'string') {
748
+ url = ParsedURL.removeWasmFunctionInfoFromURL(url);
749
+ columnNumber = parseInt(wasmCodeOffsetMatch[1], 16);
750
+ columnNumber = isNaN(columnNumber) ? undefined : columnNumber;
751
+ }
752
+ }
753
+ return { url, lineNumber, columnNumber };
754
+ }
755
+ static removeWasmFunctionInfoFromURL(url) {
756
+ const wasmFunctionRegEx = /:wasm-function\[\d+\]/;
757
+ const wasmFunctionIndex = url.search(wasmFunctionRegEx);
758
+ if (wasmFunctionIndex === -1) {
759
+ return url;
760
+ }
761
+ return ParsedURL.substring(url, 0, wasmFunctionIndex);
762
+ }
763
+ static beginsWithWindowsDriveLetter(url) {
764
+ return /^[A-Za-z]:/.test(url);
765
+ }
766
+ static beginsWithScheme(url) {
767
+ return /^[A-Za-z][A-Za-z0-9+.-]*:/.test(url);
768
+ }
769
+ static isRelativeURL(url) {
770
+ return !this.beginsWithScheme(url) || this.beginsWithWindowsDriveLetter(url);
771
+ }
772
+ isAboutBlank() {
773
+ return this.url === 'about:blank';
774
+ }
775
+ isDataURL() {
776
+ return this.scheme === 'data';
777
+ }
778
+ extractDataUrlMimeType() {
779
+ const regexp = /^data:((?<type>\w+)\/(?<subtype>\w+))?(;base64)?,/;
780
+ const match = this.url.match(regexp);
781
+ return {
782
+ type: match?.groups?.type,
783
+ subtype: match?.groups?.subtype,
784
+ };
785
+ }
786
+ isBlobURL() {
787
+ return this.url.startsWith('blob:');
788
+ }
789
+ lastPathComponentWithFragment() {
790
+ return this.lastPathComponent + (this.fragment ? '#' + this.fragment : '');
791
+ }
792
+ domain() {
793
+ if (this.isDataURL()) {
794
+ return 'data:';
795
+ }
796
+ return this.host + (this.port ? ':' + this.port : '');
797
+ }
798
+ securityOrigin() {
799
+ if (this.isDataURL()) {
800
+ return 'data:';
801
+ }
802
+ const scheme = this.isBlobURL() ? this.blobInnerScheme : this.scheme;
803
+ return scheme + '://' + this.domain();
804
+ }
805
+ urlWithoutScheme() {
806
+ if (this.scheme && this.url.startsWith(this.scheme + '://')) {
807
+ return this.url.substring(this.scheme.length + 3);
808
+ }
809
+ return this.url;
810
+ }
811
+ static urlRegexInstance = null;
812
+ }
175
813
  //# sourceMappingURL=NetworkDependencyTree.js.map