chrome-devtools-frontend 1.0.1515796 → 1.0.1515988

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 (129) hide show
  1. package/docs/contributing/infrastructure.md +131 -82
  2. package/front_end/Tests.js +3 -29
  3. package/front_end/core/common/Progress.ts +73 -55
  4. package/front_end/core/host/UserMetrics.ts +0 -1
  5. package/front_end/core/protocol_client/InspectorBackend.ts +2 -0
  6. package/front_end/core/root/Runtime.ts +0 -1
  7. package/front_end/core/sdk/CSSMatchedStyles.ts +12 -10
  8. package/front_end/core/sdk/CSSModel.ts +1 -31
  9. package/front_end/core/sdk/CSSPropertyParserMatchers.ts +27 -7
  10. package/front_end/core/sdk/DebuggerModel.ts +1 -31
  11. package/front_end/core/sdk/EnhancedTracesParser.ts +81 -50
  12. package/front_end/core/sdk/NetworkManager.ts +1 -31
  13. package/front_end/core/sdk/NetworkRequest.ts +1 -31
  14. package/front_end/core/sdk/RehydratingConnection.snapshot.txt +1003 -0
  15. package/front_end/core/sdk/RehydratingConnection.ts +13 -18
  16. package/front_end/core/sdk/RehydratingObject.ts +8 -31
  17. package/front_end/core/sdk/RemoteObject.ts +1 -31
  18. package/front_end/core/sdk/ResourceTreeModel.ts +1 -31
  19. package/front_end/core/sdk/RuntimeModel.ts +1 -31
  20. package/front_end/core/sdk/ServiceWorkerManager.ts +1 -31
  21. package/front_end/core/sdk/SourceMap.ts +1 -31
  22. package/front_end/core/sdk/TraceObject.ts +8 -3
  23. package/front_end/entrypoints/main/MainImpl.ts +0 -2
  24. package/front_end/models/ai_assistance/AiHistoryStorage.ts +1 -3
  25. package/front_end/models/ai_assistance/ConversationHandler.ts +4 -6
  26. package/front_end/models/ai_assistance/agents/AiAgent.ts +4 -1
  27. package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +107 -72
  28. package/front_end/models/ai_assistance/agents/PerformanceAnnotationsAgent.ts +2 -2
  29. package/front_end/models/ai_assistance/agents/StylingAgent.ts +2 -2
  30. package/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.snapshot.txt +178 -85
  31. package/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.ts +308 -218
  32. package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.snapshot.txt +100 -100
  33. package/front_end/models/ai_assistance/data_formatters/UnitFormatters.ts +10 -1
  34. package/front_end/models/ai_assistance/performance/AIContext.ts +19 -21
  35. package/front_end/models/bindings/ContentProviderBasedProject.ts +6 -4
  36. package/front_end/models/breakpoints/BreakpointManager.ts +3 -3
  37. package/front_end/models/har/Writer.ts +11 -11
  38. package/front_end/models/persistence/FileSystemWorkspaceBinding.ts +3 -3
  39. package/front_end/models/persistence/IsolatedFileSystem.ts +4 -4
  40. package/front_end/models/persistence/IsolatedFileSystemManager.ts +7 -7
  41. package/front_end/models/persistence/PersistenceImpl.ts +8 -8
  42. package/front_end/models/persistence/PlatformFileSystem.ts +1 -1
  43. package/front_end/models/trace/ModelImpl.ts +2 -16
  44. package/front_end/models/trace/Processor.ts +15 -9
  45. package/front_end/models/trace/handlers/AuctionWorkletsHandler.ts +4 -4
  46. package/front_end/models/trace/handlers/FramesHandler.ts +2 -2
  47. package/front_end/models/trace/handlers/LayoutShiftsHandler.ts +7 -10
  48. package/front_end/models/trace/handlers/MetaHandler.ts +11 -9
  49. package/front_end/models/trace/handlers/ScreenshotsHandler.ts +1 -1
  50. package/front_end/models/trace/handlers/ScriptsHandler.ts +5 -5
  51. package/front_end/models/trace/handlers/UserInteractionsHandler.ts +2 -14
  52. package/front_end/models/trace/handlers/UserTimingsHandler.ts +3 -4
  53. package/front_end/models/trace/insights/CLSCulprits.ts +1 -1
  54. package/front_end/models/trace/insights/DocumentLatency.ts +3 -4
  55. package/front_end/models/trace/insights/DuplicatedJavaScript.ts +1 -1
  56. package/front_end/models/trace/insights/INPBreakdown.ts +1 -1
  57. package/front_end/models/trace/insights/ImageDelivery.ts +1 -1
  58. package/front_end/models/trace/insights/LCPBreakdown.ts +1 -1
  59. package/front_end/models/trace/insights/LCPDiscovery.ts +1 -1
  60. package/front_end/models/trace/insights/ModernHTTP.ts +1 -1
  61. package/front_end/models/trace/insights/NetworkDependencyTree.ts +1 -1
  62. package/front_end/models/trace/insights/RenderBlocking.ts +1 -1
  63. package/front_end/models/trace/insights/types.ts +2 -0
  64. package/front_end/models/trace/types/TraceEvents.ts +41 -64
  65. package/front_end/models/trace_source_maps_resolver/trace_source_maps_resolver.ts +1 -1
  66. package/front_end/panels/ai_assistance/AiAssistancePanel.ts +21 -99
  67. package/front_end/panels/application/ServiceWorkersView.ts +0 -1
  68. package/front_end/panels/browser_debugger/CategorizedBreakpointsSidebarPane.ts +2 -3
  69. package/front_end/panels/common/GdpSignUpDialog.ts +6 -3
  70. package/front_end/panels/console/ConsoleView.ts +23 -28
  71. package/front_end/panels/console/ConsoleViewport.ts +2 -2
  72. package/front_end/panels/console/consoleView.css +11 -1
  73. package/front_end/panels/coverage/CoverageView.ts +2 -2
  74. package/front_end/panels/elements/ElementsTreeOutline.ts +2 -2
  75. package/front_end/panels/elements/StyleEditorWidget.ts +8 -19
  76. package/front_end/panels/elements/StylePropertyTreeElement.ts +39 -25
  77. package/front_end/panels/elements/StylesSidebarPane.ts +2 -2
  78. package/front_end/panels/elements/stylePropertiesTreeOutline.css +4 -3
  79. package/front_end/panels/layer_viewer/Layers3DView.ts +2 -2
  80. package/front_end/panels/layers/LayerTreeModel.ts +3 -3
  81. package/front_end/panels/mobile_throttling/ThrottlingSettingsTab.ts +4 -4
  82. package/front_end/panels/network/NetworkLogView.ts +1 -1
  83. package/front_end/panels/network/NetworkLogViewColumns.ts +3 -3
  84. package/front_end/panels/network/NetworkSearchScope.ts +6 -6
  85. package/front_end/panels/search/SearchView.ts +33 -32
  86. package/front_end/panels/settings/components/SyncSection.ts +6 -1
  87. package/front_end/panels/sources/SourcesSearchScope.ts +4 -4
  88. package/front_end/panels/sources/TabbedEditorContainer.ts +5 -5
  89. package/front_end/panels/timeline/TimelineFlameChartDataProvider.ts +10 -5
  90. package/front_end/panels/timeline/TimelineFlameChartView.ts +18 -15
  91. package/front_end/panels/timeline/TimelinePanel.ts +41 -22
  92. package/front_end/panels/timeline/TracingLayerTree.ts +4 -5
  93. package/front_end/panels/timeline/components/ExportTraceOptions.ts +37 -22
  94. package/front_end/panels/timeline/components/insights/BaseInsightComponent.ts +17 -7
  95. package/front_end/third_party/axe-core/README.chromium +1 -0
  96. package/front_end/third_party/codemirror/README.chromium +1 -0
  97. package/front_end/third_party/codemirror.next/README.chromium +1 -0
  98. package/front_end/third_party/csp_evaluator/README.chromium +1 -0
  99. package/front_end/third_party/diff/README.chromium +1 -0
  100. package/front_end/third_party/i18n/README.chromium +1 -0
  101. package/front_end/third_party/intl-messageformat/README.chromium +1 -0
  102. package/front_end/third_party/json5/README.chromium +1 -0
  103. package/front_end/third_party/legacy-javascript/README.chromium +1 -0
  104. package/front_end/third_party/lighthouse/README.chromium +1 -0
  105. package/front_end/third_party/lit/README.chromium +1 -0
  106. package/front_end/third_party/marked/README.chromium +1 -0
  107. package/front_end/third_party/puppeteer-replay/README.chromium +1 -0
  108. package/front_end/third_party/third-party-web/README.chromium +1 -0
  109. package/front_end/third_party/vscode.web-custom-data/README.chromium +1 -0
  110. package/front_end/third_party/wasmparser/README.chromium +1 -0
  111. package/front_end/third_party/web-vitals/README.chromium +1 -0
  112. package/front_end/ui/components/tooltips/Tooltip.ts +17 -1
  113. package/front_end/ui/legacy/ContextMenu.ts +2 -2
  114. package/front_end/ui/legacy/GlassPane.ts +7 -3
  115. package/front_end/ui/legacy/ProgressIndicator.ts +29 -16
  116. package/front_end/ui/legacy/TabbedPane.ts +2 -2
  117. package/front_end/ui/legacy/Treeoutline.ts +10 -5
  118. package/front_end/ui/legacy/UIUtils.ts +42 -10
  119. package/front_end/ui/legacy/components/color_picker/Spectrum.ts +14 -14
  120. package/front_end/ui/legacy/components/data_grid/DataGrid.ts +6 -6
  121. package/front_end/ui/legacy/components/perf_ui/FlameChart.ts +3 -29
  122. package/front_end/ui/legacy/components/source_frame/SourceFrame.ts +14 -14
  123. package/front_end/ui/visual_logging/KnownContextValues.ts +2 -0
  124. package/inspector_overlay/highlight_common.ts +1 -27
  125. package/inspector_overlay/highlight_grid_common.ts +1 -27
  126. package/inspector_overlay/tool_highlight.ts +1 -27
  127. package/inspector_overlay/tool_persistent.ts +1 -27
  128. package/inspector_overlay/tool_source_order.ts +1 -27
  129. package/package.json +1 -1
@@ -4,7 +4,7 @@
4
4
 
5
5
  import * as Common from '../../../core/common/common.js';
6
6
  import * as Trace from '../../trace/trace.js';
7
- import type {ConversationSuggestion} from '../agents/AiAgent.js';
7
+ import type {ConversationSuggestions} from '../agents/AiAgent.js';
8
8
 
9
9
  import {
10
10
  NetworkRequestFormatter,
@@ -103,7 +103,7 @@ export class PerformanceInsightFormatter {
103
103
  return this.#description().length > 0;
104
104
  }
105
105
 
106
- getSuggestions(): [ConversationSuggestion, ...ConversationSuggestion[]] {
106
+ getSuggestions(): ConversationSuggestions {
107
107
  switch (this.#insight.insightKey) {
108
108
  case 'CLSCulprits':
109
109
  return [
@@ -206,6 +206,75 @@ export class PerformanceInsightFormatter {
206
206
  return output;
207
207
  }
208
208
 
209
+ /**
210
+ * Create an AI prompt string out of the CLS Culprits Insight model to use with Ask AI.
211
+ * @param insight The CLS Culprits Model to query.
212
+ * @returns a string formatted for sending to Ask AI.
213
+ */
214
+ formatClsCulpritsInsight(insight: Trace.Insights.Models.CLSCulprits.CLSCulpritsInsightModel): string {
215
+ const {worstCluster, shifts} = insight;
216
+ if (!worstCluster) {
217
+ return '';
218
+ }
219
+
220
+ const baseTime = this.#parsedTrace.data.Meta.traceBounds.min;
221
+
222
+ const clusterTimes = {
223
+ start: worstCluster.ts - baseTime,
224
+ end: worstCluster.ts + worstCluster.dur - baseTime,
225
+ } as const;
226
+
227
+ const shiftsFormatted = worstCluster.events.map((layoutShift, index) => {
228
+ return TraceEventFormatter.layoutShift(layoutShift, index, this.#parsedTrace, shifts.get(layoutShift));
229
+ });
230
+
231
+ return `The worst layout shift cluster was the cluster that started at ${
232
+ this.#formatMicro(clusterTimes.start)} and ended at ${
233
+ this.#formatMicro(clusterTimes.end)}, with a duration of ${this.#formatMicro(worstCluster.dur)}.
234
+ The score for this cluster is ${worstCluster.clusterCumulativeScore.toFixed(4)}.
235
+
236
+ Layout shifts in this cluster:
237
+ ${shiftsFormatted.join('\n')}`;
238
+ }
239
+
240
+ /**
241
+ * Create an AI prompt string out of the Document Latency Insight model to use with Ask AI.
242
+ * @param insight The Document Latency Model to query.
243
+ * @returns a string formatted for sending to Ask AI.
244
+ */
245
+ formatDocumentLatencyInsight(insight: Trace.Insights.Models.DocumentLatency.DocumentLatencyInsightModel): string {
246
+ if (!insight.data) {
247
+ return '';
248
+ }
249
+ const {checklist, documentRequest} = insight.data;
250
+ if (!documentRequest) {
251
+ return '';
252
+ }
253
+ const checklistBulletPoints: Array<{name: string, passed: boolean}> = [];
254
+ checklistBulletPoints.push({
255
+ name: 'The request was not redirected',
256
+ passed: checklist.noRedirects.value,
257
+ });
258
+ checklistBulletPoints.push({
259
+ name: 'Server responded quickly',
260
+ passed: checklist.serverResponseIsFast.value,
261
+ });
262
+ checklistBulletPoints.push({
263
+ name: 'Compression was applied',
264
+ passed: checklist.usesCompression.value,
265
+ });
266
+
267
+ return `${this.#lcpMetricSharedContext()}
268
+
269
+ ${TraceEventFormatter.networkRequests([documentRequest], this.#parsedTrace, {
270
+ verbose: true,
271
+ customTitle: 'Document network request'
272
+ })}
273
+
274
+ The result of the checks for this insight are:
275
+ ${checklistBulletPoints.map(point => `- ${point.name}: ${point.passed ? 'PASSED' : 'FAILED'}`).join('\n')}`;
276
+ }
277
+
209
278
  /**
210
279
  * Create an AI prompt string out of the DOM Size model to use with Ask AI.
211
280
  * Note: This function accesses the UIStrings within DomSize to help build the
@@ -262,6 +331,32 @@ export class PerformanceInsightFormatter {
262
331
  return output;
263
332
  }
264
333
 
334
+ /**
335
+ * Create an AI prompt string out of the Duplicated JavaScript Insight model to use with Ask AI.
336
+ * @param insight The Duplicated JavaScript Model to query.
337
+ * @returns a string formatted for sending to Ask AI.
338
+ */
339
+ formatDuplicatedJavaScriptInsight(
340
+ insight: Trace.Insights.Models.DuplicatedJavaScript.DuplicatedJavaScriptInsightModel): string {
341
+ const totalWastedBytes = insight.wastedBytes;
342
+ const duplicatedScriptsByModule = insight.duplicationGroupedByNodeModules;
343
+
344
+ if (duplicatedScriptsByModule.size === 0) {
345
+ return 'There is no duplicated JavaScript in the page modules';
346
+ }
347
+
348
+ const filesFormatted =
349
+ Array.from(duplicatedScriptsByModule)
350
+ .map(
351
+ ([module, duplication]) =>
352
+ `- Source: ${module} - Duplicated bytes: ${duplication.estimatedDuplicateBytes} bytes`)
353
+ .join('\n');
354
+
355
+ return `Total wasted bytes: ${totalWastedBytes} bytes.
356
+
357
+ Duplication grouped by Node modules: ${filesFormatted}`;
358
+ }
359
+
265
360
  /**
266
361
  * Create an AI prompt string out of the NetworkDependencyTree Insight model to use with Ask AI.
267
362
  * Note: This function accesses the UIStrings within NetworkDependencyTree to help build the
@@ -344,6 +439,170 @@ export class PerformanceInsightFormatter {
344
439
  return output;
345
440
  }
346
441
 
442
+ /**
443
+ * Create an AI prompt string out of the INP Brekdown Insight model to use with Ask AI.
444
+ * @param insight The INP Breakdown Model to query.
445
+ * @returns a string formatted for sending to Ask AI.
446
+ */
447
+ formatImageDeliveryInsight(insight: Trace.Insights.Models.ImageDelivery.ImageDeliveryInsightModel): string {
448
+ const optimizableImages = insight.optimizableImages;
449
+ if (optimizableImages.length === 0) {
450
+ return 'There are no unoptimized images on this page.';
451
+ }
452
+
453
+ const imageDetails = optimizableImages
454
+ .map(image => {
455
+ // List potential optimizations for the image
456
+ const optimizations =
457
+ image.optimizations
458
+ .map(optimization => {
459
+ const message =
460
+ Trace.Insights.Models.ImageDelivery.getOptimizationMessage(optimization);
461
+ const byteSavings = bytes(optimization.byteSavings);
462
+ return `${message} (Est ${byteSavings})`;
463
+ })
464
+ .join('\n');
465
+
466
+ return `### ${image.request.args.data.url}
467
+ - Potential savings: ${bytes(image.byteSavings)}
468
+ - Optimizations:\n${optimizations}`;
469
+ })
470
+ .join('\n\n');
471
+
472
+ return `Total potential savings: ${bytes(insight.wastedBytes)}
473
+
474
+ The following images could be optimized:\n\n${imageDetails}`;
475
+ }
476
+
477
+ /**
478
+ * Create an AI prompt string out of the INP Brekdown Insight model to use with Ask AI.
479
+ * @param insight The INP Breakdown Model to query.
480
+ * @returns a string formatted for sending to Ask AI.
481
+ */
482
+ formatInpBreakdownInsight(insight: Trace.Insights.Models.INPBreakdown.INPBreakdownInsightModel): string {
483
+ const event = insight.longestInteractionEvent;
484
+ if (!event) {
485
+ return '';
486
+ }
487
+
488
+ const inpInfoForEvent =
489
+ `The longest interaction on the page was a \`${event.type}\` which had a total duration of \`${
490
+ this.#formatMicro(event.dur)}\`. The timings of each of the three phases were:
491
+
492
+ 1. Input delay: ${this.#formatMicro(event.inputDelay)}
493
+ 2. Processing duration: ${this.#formatMicro(event.mainThreadHandling)}
494
+ 3. Presentation delay: ${this.#formatMicro(event.presentationDelay)}.`;
495
+
496
+ return inpInfoForEvent;
497
+ }
498
+
499
+ /**
500
+ * Create an AI prompt string out of the LCP Brekdown Insight model to use with Ask AI.
501
+ * @param insight The LCP Breakdown Model to query.
502
+ * @returns a string formatted for sending to Ask AI.
503
+ */
504
+ formatLcpBreakdownInsight(insight: Trace.Insights.Models.LCPBreakdown.LCPBreakdownInsightModel): string {
505
+ const {subparts, lcpMs} = insight;
506
+ if (!lcpMs || !subparts) {
507
+ return '';
508
+ }
509
+
510
+ // Text based LCP has TTFB & Render delay
511
+ // Image based has TTFB, Load delay, Load time and Render delay
512
+ // Note that we expect every trace + LCP to have TTFB + Render delay, but
513
+ // very old traces are missing the data, so we have to code defensively
514
+ // in case the subparts are not present.
515
+ const phaseBulletPoints: Array<{name: string, value: string, percentage: string}> = [];
516
+
517
+ Object.values(subparts).forEach((subpart: Trace.Insights.Models.LCPBreakdown.Subpart) => {
518
+ const phaseMilli = Trace.Helpers.Timing.microToMilli(subpart.range);
519
+ const percentage = (phaseMilli / lcpMs * 100).toFixed(1);
520
+ phaseBulletPoints.push({name: subpart.label, value: this.#formatMilli(phaseMilli), percentage});
521
+ });
522
+
523
+ return `${this.#lcpMetricSharedContext()}
524
+
525
+ We can break this time down into the ${phaseBulletPoints.length} phases that combine to make the LCP time:
526
+
527
+ ${
528
+ phaseBulletPoints.map(phase => `- ${phase.name}: ${phase.value} (${phase.percentage}% of total LCP time)`)
529
+ .join('\n')}`;
530
+ }
531
+
532
+ /**
533
+ * Create an AI prompt string out of the LCP Brekdown Insight model to use with Ask AI.
534
+ * @param insight The LCP Breakdown Model to query.
535
+ * @returns a string formatted for sending to Ask AI.
536
+ */
537
+ formatLcpDiscoveryInsight(insight: Trace.Insights.Models.LCPDiscovery.LCPDiscoveryInsightModel): string {
538
+ const {checklist, lcpEvent, lcpRequest, earliestDiscoveryTimeTs} = insight;
539
+ if (!checklist || !lcpEvent || !lcpRequest || !earliestDiscoveryTimeTs) {
540
+ return '';
541
+ }
542
+
543
+ const checklistBulletPoints: Array<{name: string, passed: boolean}> = [];
544
+ checklistBulletPoints.push({
545
+ name: checklist.priorityHinted.label,
546
+ passed: checklist.priorityHinted.value,
547
+ });
548
+ checklistBulletPoints.push({
549
+ name: checklist.eagerlyLoaded.label,
550
+ passed: checklist.eagerlyLoaded.value,
551
+ });
552
+ checklistBulletPoints.push({
553
+ name: checklist.requestDiscoverable.label,
554
+ passed: checklist.requestDiscoverable.value,
555
+ });
556
+
557
+ return `${this.#lcpMetricSharedContext()}
558
+
559
+ The result of the checks for this insight are:
560
+ ${checklistBulletPoints.map(point => `- ${point.name}: ${point.passed ? 'PASSED' : 'FAILED'}`).join('\n')}`;
561
+ }
562
+
563
+ /**
564
+ * Create an AI prompt string out of the Legacy JavaScript Insight model to use with Ask AI.
565
+ * @param insight The Legacy JavaScript Model to query.
566
+ * @returns a string formatted for sending to Ask AI.
567
+ */
568
+ formatLegacyJavaScriptInsight(insight: Trace.Insights.Models.LegacyJavaScript.LegacyJavaScriptInsightModel): string {
569
+ const legacyJavaScriptResults = insight.legacyJavaScriptResults;
570
+
571
+ if (legacyJavaScriptResults.size === 0) {
572
+ return 'There is no significant amount of legacy JavaScript on the page.';
573
+ }
574
+
575
+ const filesFormatted =
576
+ Array.from(legacyJavaScriptResults)
577
+ .map(([script, result]) => `\n- Script: ${script.url} - Wasted bytes: ${result.estimatedByteSavings} bytes
578
+ Matches:
579
+ ${result.matches.map(match => `Line: ${match.line}, Column: ${match.column}, Name: ${match.name}`).join('\n')}`)
580
+ .join('\n');
581
+
582
+ return `Total legacy JavaScript: ${legacyJavaScriptResults.size} files.
583
+
584
+ Legacy JavaScript by file:
585
+ ${filesFormatted}`;
586
+ }
587
+
588
+ /**
589
+ * Create an AI prompt string out of the Modern HTTP Insight model to use with Ask AI.
590
+ * @param insight The Modern HTTP Model to query.
591
+ * @returns a string formatted for sending to Ask AI.
592
+ */
593
+ formatModernHttpInsight(insight: Trace.Insights.Models.ModernHTTP.ModernHTTPInsightModel): string {
594
+ const requestSummary = (insight.http1Requests.length === 1) ?
595
+ TraceEventFormatter.networkRequests(insight.http1Requests, this.#parsedTrace, {verbose: true}) :
596
+ TraceEventFormatter.networkRequests(insight.http1Requests, this.#parsedTrace);
597
+
598
+ if (requestSummary.length === 0) {
599
+ return 'There are no requests that were served over a legacy HTTP protocol.';
600
+ }
601
+
602
+ return `Here is a list of the network requests that were served over a legacy HTTP protocol:
603
+ ${requestSummary}`;
604
+ }
605
+
347
606
  /**
348
607
  * Create an AI prompt string out of the NetworkDependencyTree Insight model to use with Ask AI.
349
608
  * Note: This function accesses the UIStrings within NetworkDependencyTree to help build the
@@ -425,6 +684,23 @@ export class PerformanceInsightFormatter {
425
684
  return output;
426
685
  }
427
686
 
687
+ /**
688
+ * Create an AI prompt string out of the Render Blocking Insight model to use with Ask AI.
689
+ * @param insight The Render Blocking Model to query.
690
+ * @returns a string formatted for sending to Ask AI.
691
+ */
692
+ formatRenderBlockingInsight(insight: Trace.Insights.Models.RenderBlocking.RenderBlockingInsightModel): string {
693
+ const requestSummary = TraceEventFormatter.networkRequests(insight.renderBlockingRequests, this.#parsedTrace);
694
+
695
+ if (requestSummary.length === 0) {
696
+ return 'There are no network requests that are render blocking.';
697
+ }
698
+
699
+ return `Here is a list of the network requests that were render blocking on this page and their duration:
700
+
701
+ ${requestSummary}`;
702
+ }
703
+
428
704
  /**
429
705
  * Create an AI prompt string out of the Slow CSS Selector Insight model to use with Ask AI.
430
706
  * Note: This function accesses the UIStrings within SlowCSSSelector to help build the
@@ -563,250 +839,64 @@ ${this.#links()}`;
563
839
  }
564
840
 
565
841
  #details(): string {
566
- if (Trace.Insights.Models.ImageDelivery.isImageDelivery(this.#insight)) {
567
- const optimizableImages = this.#insight.optimizableImages;
568
- if (optimizableImages.length === 0) {
569
- return 'There are no unoptimized images on this page.';
570
- }
571
-
572
- const imageDetails = optimizableImages
573
- .map(image => {
574
- // List potential optimizations for the image
575
- const optimizations =
576
- image.optimizations
577
- .map(optimization => {
578
- const message =
579
- Trace.Insights.Models.ImageDelivery.getOptimizationMessage(optimization);
580
- const byteSavings = bytes(optimization.byteSavings);
581
- return `${message} (Est ${byteSavings})`;
582
- })
583
- .join('\n');
584
-
585
- return `### ${image.request.args.data.url}
586
- - Potential savings: ${bytes(image.byteSavings)}
587
- - Optimizations:\n${optimizations}`;
588
- })
589
- .join('\n\n');
590
-
591
- return `Total potential savings: ${bytes(this.#insight.wastedBytes)}
592
-
593
- The following images could be optimized:\n\n${imageDetails}`;
842
+ if (Trace.Insights.Models.Cache.isCacheInsight(this.#insight)) {
843
+ return this.formatCacheInsight(this.#insight);
594
844
  }
595
845
 
596
- if (Trace.Insights.Models.LCPBreakdown.isLCPBreakdown(this.#insight)) {
597
- const {subparts, lcpMs} = this.#insight;
598
- if (!lcpMs || !subparts) {
599
- return '';
600
- }
601
-
602
- // Text based LCP has TTFB & Render delay
603
- // Image based has TTFB, Load delay, Load time and Render delay
604
- // Note that we expect every trace + LCP to have TTFB + Render delay, but
605
- // very old traces are missing the data, so we have to code defensively
606
- // in case the subparts are not present.
607
- const phaseBulletPoints: Array<{name: string, value: string, percentage: string}> = [];
608
-
609
- Object.values(subparts).forEach((subpart: Trace.Insights.Models.LCPBreakdown.Subpart) => {
610
- const phaseMilli = Trace.Helpers.Timing.microToMilli(subpart.range);
611
- const percentage = (phaseMilli / lcpMs * 100).toFixed(1);
612
- phaseBulletPoints.push({name: subpart.label, value: this.#formatMilli(phaseMilli), percentage});
613
- });
614
-
615
- return `${this.#lcpMetricSharedContext()}
616
-
617
- We can break this time down into the ${phaseBulletPoints.length} phases that combine to make the LCP time:
618
-
619
- ${
620
- phaseBulletPoints.map(phase => `- ${phase.name}: ${phase.value} (${phase.percentage}% of total LCP time)`)
621
- .join('\n')}`;
846
+ if (Trace.Insights.Models.CLSCulprits.isCLSCulpritsInsight(this.#insight)) {
847
+ return this.formatClsCulpritsInsight(this.#insight);
622
848
  }
623
849
 
624
- if (Trace.Insights.Models.LCPDiscovery.isLCPDiscovery(this.#insight)) {
625
- const {checklist, lcpEvent, lcpRequest, earliestDiscoveryTimeTs} = this.#insight;
626
- if (!checklist || !lcpEvent || !lcpRequest || !earliestDiscoveryTimeTs) {
627
- return '';
628
- }
629
-
630
- const checklistBulletPoints: Array<{name: string, passed: boolean}> = [];
631
- checklistBulletPoints.push({
632
- name: checklist.priorityHinted.label,
633
- passed: checklist.priorityHinted.value,
634
- });
635
- checklistBulletPoints.push({
636
- name: checklist.eagerlyLoaded.label,
637
- passed: checklist.eagerlyLoaded.value,
638
- });
639
- checklistBulletPoints.push({
640
- name: checklist.requestDiscoverable.label,
641
- passed: checklist.requestDiscoverable.value,
642
- });
643
-
644
- return `${this.#lcpMetricSharedContext()}
645
-
646
- The result of the checks for this insight are:
647
- ${checklistBulletPoints.map(point => `- ${point.name}: ${point.passed ? 'PASSED' : 'FAILED'}`).join('\n')}`;
850
+ if (Trace.Insights.Models.DocumentLatency.isDocumentLatencyInsight(this.#insight)) {
851
+ return this.formatDocumentLatencyInsight(this.#insight);
648
852
  }
649
853
 
650
- if (Trace.Insights.Models.RenderBlocking.isRenderBlocking(this.#insight)) {
651
- const requestSummary =
652
- TraceEventFormatter.networkRequests(this.#insight.renderBlockingRequests, this.#parsedTrace);
653
-
654
- if (requestSummary.length === 0) {
655
- return 'There are no network requests that are render blocking.';
656
- }
657
-
658
- return `Here is a list of the network requests that were render blocking on this page and their duration:
659
-
660
- ${requestSummary}`;
854
+ if (Trace.Insights.Models.DOMSize.isDomSizeInsight(this.#insight)) {
855
+ return this.formatDomSizeInsight(this.#insight);
661
856
  }
662
857
 
663
- if (Trace.Insights.Models.DocumentLatency.isDocumentLatency(this.#insight)) {
664
- if (!this.#insight.data) {
665
- return '';
666
- }
667
- const {checklist, documentRequest} = this.#insight.data;
668
- if (!documentRequest) {
669
- return '';
670
- }
671
- const checklistBulletPoints: Array<{name: string, passed: boolean}> = [];
672
- checklistBulletPoints.push({
673
- name: 'The request was not redirected',
674
- passed: checklist.noRedirects.value,
675
- });
676
- checklistBulletPoints.push({
677
- name: 'Server responded quickly',
678
- passed: checklist.serverResponseIsFast.value,
679
- });
680
- checklistBulletPoints.push({
681
- name: 'Compression was applied',
682
- passed: checklist.usesCompression.value,
683
- });
684
-
685
- return `${this.#lcpMetricSharedContext()}
686
-
687
- ${TraceEventFormatter.networkRequests([documentRequest], this.#parsedTrace, {
688
- verbose: true,
689
- customTitle: 'Document network request'
690
- })}
691
-
692
- The result of the checks for this insight are:
693
- ${checklistBulletPoints.map(point => `- ${point.name}: ${point.passed ? 'PASSED' : 'FAILED'}`).join('\n')}`;
858
+ if (Trace.Insights.Models.DuplicatedJavaScript.isDuplicatedJavaScriptInsight(this.#insight)) {
859
+ return this.formatDuplicatedJavaScriptInsight(this.#insight);
694
860
  }
695
861
 
696
- if (Trace.Insights.Models.INPBreakdown.isINPBreakdown(this.#insight)) {
697
- const event = this.#insight.longestInteractionEvent;
698
- if (!event) {
699
- return '';
700
- }
701
-
702
- const inpInfoForEvent =
703
- `The longest interaction on the page was a \`${event.type}\` which had a total duration of \`${
704
- this.#formatMicro(event.dur)}\`. The timings of each of the three phases were:
705
-
706
- 1. Input delay: ${this.#formatMicro(event.inputDelay)}
707
- 2. Processing duration: ${this.#formatMicro(event.mainThreadHandling)}
708
- 3. Presentation delay: ${this.#formatMicro(event.presentationDelay)}.`;
709
-
710
- return inpInfoForEvent;
862
+ if (Trace.Insights.Models.FontDisplay.isFontDisplayInsight(this.#insight)) {
863
+ return this.formatFontDisplayInsight(this.#insight);
711
864
  }
712
865
 
713
- if (Trace.Insights.Models.CLSCulprits.isCLSCulprits(this.#insight)) {
714
- const {worstCluster, shifts} = this.#insight;
715
- if (!worstCluster) {
716
- return '';
717
- }
718
-
719
- const baseTime = this.#parsedTrace.data.Meta.traceBounds.min;
720
-
721
- const clusterTimes = {
722
- start: worstCluster.ts - baseTime,
723
- end: worstCluster.ts + worstCluster.dur - baseTime,
724
- } as const;
725
-
726
- const shiftsFormatted = worstCluster.events.map((layoutShift, index) => {
727
- return TraceEventFormatter.layoutShift(layoutShift, index, this.#parsedTrace, shifts.get(layoutShift));
728
- });
729
-
730
- return `The worst layout shift cluster was the cluster that started at ${
731
- this.#formatMicro(clusterTimes.start)} and ended at ${
732
- this.#formatMicro(clusterTimes.end)}, with a duration of ${this.#formatMicro(worstCluster.dur)}.
733
- The score for this cluster is ${worstCluster.clusterCumulativeScore.toFixed(4)}.
734
-
735
- Layout shifts in this cluster:
736
- ${shiftsFormatted.join('\n')}`;
866
+ if (Trace.Insights.Models.ForcedReflow.isForcedReflowInsight(this.#insight)) {
867
+ return this.formatForcedReflowInsight(this.#insight);
737
868
  }
738
869
 
739
- if (Trace.Insights.Models.ModernHTTP.isModernHTTP(this.#insight)) {
740
- const requestSummary = (this.#insight.http1Requests.length === 1) ?
741
- TraceEventFormatter.networkRequests(this.#insight.http1Requests, this.#parsedTrace, {verbose: true}) :
742
- TraceEventFormatter.networkRequests(this.#insight.http1Requests, this.#parsedTrace);
743
-
744
- if (requestSummary.length === 0) {
745
- return 'There are no requests that were served over a legacy HTTP protocol.';
746
- }
747
-
748
- return `Here is a list of the network requests that were served over a legacy HTTP protocol:
749
- ${requestSummary}`;
870
+ if (Trace.Insights.Models.ImageDelivery.isImageDeliveryInsight(this.#insight)) {
871
+ return this.formatImageDeliveryInsight(this.#insight);
750
872
  }
751
873
 
752
- if (Trace.Insights.Models.DuplicatedJavaScript.isDuplicatedJavaScript(this.#insight)) {
753
- const totalWastedBytes = this.#insight.wastedBytes;
754
- const duplicatedScriptsByModule = this.#insight.duplicationGroupedByNodeModules;
755
-
756
- if (duplicatedScriptsByModule.size === 0) {
757
- return 'There is no duplicated JavaScript in the page modules';
758
- }
759
-
760
- const filesFormatted =
761
- Array.from(duplicatedScriptsByModule)
762
- .map(
763
- ([module, duplication]) =>
764
- `- Source: ${module} - Duplicated bytes: ${duplication.estimatedDuplicateBytes} bytes`)
765
- .join('\n');
766
-
767
- return `Total wasted bytes: ${totalWastedBytes} bytes.
768
-
769
- Duplication grouped by Node modules: ${filesFormatted}`;
874
+ if (Trace.Insights.Models.INPBreakdown.isINPBreakdownInsight(this.#insight)) {
875
+ return this.formatInpBreakdownInsight(this.#insight);
770
876
  }
771
877
 
772
- if (Trace.Insights.Models.LegacyJavaScript.isLegacyJavaScript(this.#insight)) {
773
- const legacyJavaScriptResults = this.#insight.legacyJavaScriptResults;
774
-
775
- if (legacyJavaScriptResults.size === 0) {
776
- return 'There is no significant amount of legacy JavaScript on the page.';
777
- }
778
-
779
- const filesFormatted =
780
- Array.from(legacyJavaScriptResults)
781
- .map(([script, result]) => `\n- Script: ${script.url} - Wasted bytes: ${result.estimatedByteSavings} bytes
782
- Matches:
783
- ${result.matches.map(match => `Line: ${match.line}, Column: ${match.column}, Name: ${match.name}`).join('\n')}`)
784
- .join('\n');
785
-
786
- return `Total legacy JavaScript: ${legacyJavaScriptResults.size} files.
787
-
788
- Legacy JavaScript by file:
789
- ${filesFormatted}`;
878
+ if (Trace.Insights.Models.LCPBreakdown.isLCPBreakdownInsight(this.#insight)) {
879
+ return this.formatLcpBreakdownInsight(this.#insight);
790
880
  }
791
881
 
792
- if (Trace.Insights.Models.Cache.isCacheInsight(this.#insight)) {
793
- return this.formatCacheInsight(this.#insight);
882
+ if (Trace.Insights.Models.LCPDiscovery.isLCPDiscoveryInsight(this.#insight)) {
883
+ return this.formatLcpDiscoveryInsight(this.#insight);
794
884
  }
795
885
 
796
- if (Trace.Insights.Models.DOMSize.isDomSizeInsight(this.#insight)) {
797
- return this.formatDomSizeInsight(this.#insight);
886
+ if (Trace.Insights.Models.LegacyJavaScript.isLegacyJavaScript(this.#insight)) {
887
+ return this.formatLegacyJavaScriptInsight(this.#insight);
798
888
  }
799
889
 
800
- if (Trace.Insights.Models.FontDisplay.isFontDisplayInsight(this.#insight)) {
801
- return this.formatFontDisplayInsight(this.#insight);
890
+ if (Trace.Insights.Models.ModernHTTP.isModernHTTPInsight(this.#insight)) {
891
+ return this.formatModernHttpInsight(this.#insight);
802
892
  }
803
893
 
804
- if (Trace.Insights.Models.ForcedReflow.isForcedReflowInsight(this.#insight)) {
805
- return this.formatForcedReflowInsight(this.#insight);
894
+ if (Trace.Insights.Models.NetworkDependencyTree.isNetworkDependencyTreeInsight(this.#insight)) {
895
+ return this.formatNetworkDependencyTreeInsight(this.#insight);
806
896
  }
807
897
 
808
- if (Trace.Insights.Models.NetworkDependencyTree.isNetworkDependencyTree(this.#insight)) {
809
- return this.formatNetworkDependencyTreeInsight(this.#insight);
898
+ if (Trace.Insights.Models.RenderBlocking.isRenderBlockingInsight(this.#insight)) {
899
+ return this.formatRenderBlockingInsight(this.#insight);
810
900
  }
811
901
 
812
902
  if (Trace.Insights.Models.SlowCSSSelector.isSlowCSSSelectorInsight(this.#insight)) {