chrome-devtools-mcp 0.2.0 → 0.2.2

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 (62) hide show
  1. package/README.md +27 -8
  2. package/build/node_modules/chrome-devtools-frontend/front_end/core/common/Progress.js +60 -53
  3. package/build/node_modules/chrome-devtools-frontend/front_end/core/host/GdpClient.js +1 -1
  4. package/build/node_modules/chrome-devtools-frontend/front_end/core/host/UserMetrics.js +5 -2
  5. package/build/node_modules/chrome-devtools-frontend/front_end/core/protocol_client/InspectorBackend.js +2 -0
  6. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSMatchedStyles.js +11 -10
  7. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSModel.js +1 -1
  8. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSPropertyParserMatchers.js +24 -4
  9. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/DebuggerModel.js +1 -1
  10. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/EnhancedTracesParser.js +29 -24
  11. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/NetworkManager.js +1 -1
  12. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/NetworkRequest.js +1 -1
  13. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/RehydratingConnection.js +9 -15
  14. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/RemoteObject.js +1 -1
  15. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/ResourceTreeModel.js +1 -1
  16. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/RuntimeModel.js +1 -1
  17. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/ServiceWorkerManager.js +1 -1
  18. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/SourceMap.js +4 -31
  19. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/TraceObject.js +5 -2
  20. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/NetworkRequestFormatter.js +6 -4
  21. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.js +259 -179
  22. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/UnitFormatters.js +10 -1
  23. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AIContext.js +14 -3
  24. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/ContentProviderBasedProject.js +6 -4
  25. package/build/node_modules/chrome-devtools-frontend/front_end/models/formatter/FormatterWorkerPool.js +2 -2
  26. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/ModelImpl.js +4 -9
  27. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/Processor.js +17 -9
  28. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/AuctionWorkletsHandler.js +1 -1
  29. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/FramesHandler.js +2 -2
  30. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/LayoutShiftsHandler.js +3 -4
  31. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/MetaHandler.js +10 -9
  32. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/ScreenshotsHandler.js +0 -1
  33. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/ScriptsHandler.js +4 -4
  34. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/UserInteractionsHandler.js +2 -10
  35. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/UserTimingsHandler.js +3 -4
  36. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/helpers/SamplesIntegrator.js +8 -6
  37. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/CLSCulprits.js +1 -1
  38. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/DocumentLatency.js +3 -3
  39. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/DuplicatedJavaScript.js +1 -1
  40. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/INPBreakdown.js +1 -1
  41. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/ImageDelivery.js +1 -1
  42. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/LCPBreakdown.js +1 -1
  43. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/LCPDiscovery.js +1 -1
  44. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/ModernHTTP.js +1 -1
  45. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/NetworkDependencyTree.js +1 -1
  46. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/RenderBlocking.js +1 -1
  47. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/types/TraceEvents.js +21 -21
  48. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace_source_maps_resolver/SourceMapsResolver.js +5 -3
  49. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace_source_maps_resolver/trace_source_maps_resolver.js +1 -1
  50. package/build/src/McpContext.js +60 -10
  51. package/build/src/McpResponse.js +5 -4
  52. package/build/src/WaitForHelper.js +127 -0
  53. package/build/src/browser.js +12 -9
  54. package/build/src/index.js +20 -21
  55. package/build/src/logger.js +1 -0
  56. package/build/src/tools/input.js +5 -6
  57. package/build/src/tools/pages.js +2 -3
  58. package/build/src/tools/performance.js +16 -14
  59. package/build/src/tools/script.js +1 -2
  60. package/build/src/trace-processing/parse.js +23 -14
  61. package/package.json +15 -16
  62. package/build/src/waitForHelpers.js +0 -109
@@ -177,6 +177,66 @@ export class PerformanceInsightFormatter {
177
177
  output += '\n\n' + Trace.Insights.Models.Cache.UIStrings.description;
178
178
  return output;
179
179
  }
180
+ /**
181
+ * Create an AI prompt string out of the CLS Culprits Insight model to use with Ask AI.
182
+ * @param insight The CLS Culprits Model to query.
183
+ * @returns a string formatted for sending to Ask AI.
184
+ */
185
+ formatClsCulpritsInsight(insight) {
186
+ const { worstCluster, shifts } = insight;
187
+ if (!worstCluster) {
188
+ return '';
189
+ }
190
+ const baseTime = this.#parsedTrace.data.Meta.traceBounds.min;
191
+ const clusterTimes = {
192
+ start: worstCluster.ts - baseTime,
193
+ end: worstCluster.ts + worstCluster.dur - baseTime,
194
+ };
195
+ const shiftsFormatted = worstCluster.events.map((layoutShift, index) => {
196
+ return TraceEventFormatter.layoutShift(layoutShift, index, this.#parsedTrace, shifts.get(layoutShift));
197
+ });
198
+ return `The worst layout shift cluster was the cluster that started at ${this.#formatMicro(clusterTimes.start)} and ended at ${this.#formatMicro(clusterTimes.end)}, with a duration of ${this.#formatMicro(worstCluster.dur)}.
199
+ The score for this cluster is ${worstCluster.clusterCumulativeScore.toFixed(4)}.
200
+
201
+ Layout shifts in this cluster:
202
+ ${shiftsFormatted.join('\n')}`;
203
+ }
204
+ /**
205
+ * Create an AI prompt string out of the Document Latency Insight model to use with Ask AI.
206
+ * @param insight The Document Latency Model to query.
207
+ * @returns a string formatted for sending to Ask AI.
208
+ */
209
+ formatDocumentLatencyInsight(insight) {
210
+ if (!insight.data) {
211
+ return '';
212
+ }
213
+ const { checklist, documentRequest } = insight.data;
214
+ if (!documentRequest) {
215
+ return '';
216
+ }
217
+ const checklistBulletPoints = [];
218
+ checklistBulletPoints.push({
219
+ name: 'The request was not redirected',
220
+ passed: checklist.noRedirects.value,
221
+ });
222
+ checklistBulletPoints.push({
223
+ name: 'Server responded quickly',
224
+ passed: checklist.serverResponseIsFast.value,
225
+ });
226
+ checklistBulletPoints.push({
227
+ name: 'Compression was applied',
228
+ passed: checklist.usesCompression.value,
229
+ });
230
+ return `${this.#lcpMetricSharedContext()}
231
+
232
+ ${TraceEventFormatter.networkRequests([documentRequest], this.#parsedTrace, {
233
+ verbose: true,
234
+ customTitle: 'Document network request'
235
+ })}
236
+
237
+ The result of the checks for this insight are:
238
+ ${checklistBulletPoints.map(point => `- ${point.name}: ${point.passed ? 'PASSED' : 'FAILED'}`).join('\n')}`;
239
+ }
180
240
  /**
181
241
  * Create an AI prompt string out of the DOM Size model to use with Ask AI.
182
242
  * Note: This function accesses the UIStrings within DomSize to help build the
@@ -224,6 +284,24 @@ export class PerformanceInsightFormatter {
224
284
  }
225
285
  return output;
226
286
  }
287
+ /**
288
+ * Create an AI prompt string out of the Duplicated JavaScript Insight model to use with Ask AI.
289
+ * @param insight The Duplicated JavaScript Model to query.
290
+ * @returns a string formatted for sending to Ask AI.
291
+ */
292
+ formatDuplicatedJavaScriptInsight(insight) {
293
+ const totalWastedBytes = insight.wastedBytes;
294
+ const duplicatedScriptsByModule = insight.duplicationGroupedByNodeModules;
295
+ if (duplicatedScriptsByModule.size === 0) {
296
+ return 'There is no duplicated JavaScript in the page modules';
297
+ }
298
+ const filesFormatted = Array.from(duplicatedScriptsByModule)
299
+ .map(([module, duplication]) => `- Source: ${module} - Duplicated bytes: ${duplication.estimatedDuplicateBytes} bytes`)
300
+ .join('\n');
301
+ return `Total wasted bytes: ${totalWastedBytes} bytes.
302
+
303
+ Duplication grouped by Node modules: ${filesFormatted}`;
304
+ }
227
305
  /**
228
306
  * Create an AI prompt string out of the NetworkDependencyTree Insight model to use with Ask AI.
229
307
  * Note: This function accesses the UIStrings within NetworkDependencyTree to help build the
@@ -297,6 +375,143 @@ export class PerformanceInsightFormatter {
297
375
  }
298
376
  return output;
299
377
  }
378
+ /**
379
+ * Create an AI prompt string out of the INP Brekdown Insight model to use with Ask AI.
380
+ * @param insight The INP Breakdown Model to query.
381
+ * @returns a string formatted for sending to Ask AI.
382
+ */
383
+ formatImageDeliveryInsight(insight) {
384
+ const optimizableImages = insight.optimizableImages;
385
+ if (optimizableImages.length === 0) {
386
+ return 'There are no unoptimized images on this page.';
387
+ }
388
+ const imageDetails = optimizableImages
389
+ .map(image => {
390
+ // List potential optimizations for the image
391
+ const optimizations = image.optimizations
392
+ .map(optimization => {
393
+ const message = Trace.Insights.Models.ImageDelivery.getOptimizationMessage(optimization);
394
+ const byteSavings = bytes(optimization.byteSavings);
395
+ return `${message} (Est ${byteSavings})`;
396
+ })
397
+ .join('\n');
398
+ return `### ${image.request.args.data.url}
399
+ - Potential savings: ${bytes(image.byteSavings)}
400
+ - Optimizations:\n${optimizations}`;
401
+ })
402
+ .join('\n\n');
403
+ return `Total potential savings: ${bytes(insight.wastedBytes)}
404
+
405
+ The following images could be optimized:\n\n${imageDetails}`;
406
+ }
407
+ /**
408
+ * Create an AI prompt string out of the INP Brekdown Insight model to use with Ask AI.
409
+ * @param insight The INP Breakdown Model to query.
410
+ * @returns a string formatted for sending to Ask AI.
411
+ */
412
+ formatInpBreakdownInsight(insight) {
413
+ const event = insight.longestInteractionEvent;
414
+ if (!event) {
415
+ return '';
416
+ }
417
+ const inpInfoForEvent = `The longest interaction on the page was a \`${event.type}\` which had a total duration of \`${this.#formatMicro(event.dur)}\`. The timings of each of the three phases were:
418
+
419
+ 1. Input delay: ${this.#formatMicro(event.inputDelay)}
420
+ 2. Processing duration: ${this.#formatMicro(event.mainThreadHandling)}
421
+ 3. Presentation delay: ${this.#formatMicro(event.presentationDelay)}.`;
422
+ return inpInfoForEvent;
423
+ }
424
+ /**
425
+ * Create an AI prompt string out of the LCP Brekdown Insight model to use with Ask AI.
426
+ * @param insight The LCP Breakdown Model to query.
427
+ * @returns a string formatted for sending to Ask AI.
428
+ */
429
+ formatLcpBreakdownInsight(insight) {
430
+ const { subparts, lcpMs } = insight;
431
+ if (!lcpMs || !subparts) {
432
+ return '';
433
+ }
434
+ // Text based LCP has TTFB & Render delay
435
+ // Image based has TTFB, Load delay, Load time and Render delay
436
+ // Note that we expect every trace + LCP to have TTFB + Render delay, but
437
+ // very old traces are missing the data, so we have to code defensively
438
+ // in case the subparts are not present.
439
+ const phaseBulletPoints = [];
440
+ Object.values(subparts).forEach((subpart) => {
441
+ const phaseMilli = Trace.Helpers.Timing.microToMilli(subpart.range);
442
+ const percentage = (phaseMilli / lcpMs * 100).toFixed(1);
443
+ phaseBulletPoints.push({ name: subpart.label, value: this.#formatMilli(phaseMilli), percentage });
444
+ });
445
+ return `${this.#lcpMetricSharedContext()}
446
+
447
+ We can break this time down into the ${phaseBulletPoints.length} phases that combine to make the LCP time:
448
+
449
+ ${phaseBulletPoints.map(phase => `- ${phase.name}: ${phase.value} (${phase.percentage}% of total LCP time)`)
450
+ .join('\n')}`;
451
+ }
452
+ /**
453
+ * Create an AI prompt string out of the LCP Brekdown Insight model to use with Ask AI.
454
+ * @param insight The LCP Breakdown Model to query.
455
+ * @returns a string formatted for sending to Ask AI.
456
+ */
457
+ formatLcpDiscoveryInsight(insight) {
458
+ const { checklist, lcpEvent, lcpRequest, earliestDiscoveryTimeTs } = insight;
459
+ if (!checklist || !lcpEvent || !lcpRequest || !earliestDiscoveryTimeTs) {
460
+ return '';
461
+ }
462
+ const checklistBulletPoints = [];
463
+ checklistBulletPoints.push({
464
+ name: checklist.priorityHinted.label,
465
+ passed: checklist.priorityHinted.value,
466
+ });
467
+ checklistBulletPoints.push({
468
+ name: checklist.eagerlyLoaded.label,
469
+ passed: checklist.eagerlyLoaded.value,
470
+ });
471
+ checklistBulletPoints.push({
472
+ name: checklist.requestDiscoverable.label,
473
+ passed: checklist.requestDiscoverable.value,
474
+ });
475
+ return `${this.#lcpMetricSharedContext()}
476
+
477
+ The result of the checks for this insight are:
478
+ ${checklistBulletPoints.map(point => `- ${point.name}: ${point.passed ? 'PASSED' : 'FAILED'}`).join('\n')}`;
479
+ }
480
+ /**
481
+ * Create an AI prompt string out of the Legacy JavaScript Insight model to use with Ask AI.
482
+ * @param insight The Legacy JavaScript Model to query.
483
+ * @returns a string formatted for sending to Ask AI.
484
+ */
485
+ formatLegacyJavaScriptInsight(insight) {
486
+ const legacyJavaScriptResults = insight.legacyJavaScriptResults;
487
+ if (legacyJavaScriptResults.size === 0) {
488
+ return 'There is no significant amount of legacy JavaScript on the page.';
489
+ }
490
+ const filesFormatted = Array.from(legacyJavaScriptResults)
491
+ .map(([script, result]) => `\n- Script: ${script.url} - Wasted bytes: ${result.estimatedByteSavings} bytes
492
+ Matches:
493
+ ${result.matches.map(match => `Line: ${match.line}, Column: ${match.column}, Name: ${match.name}`).join('\n')}`)
494
+ .join('\n');
495
+ return `Total legacy JavaScript: ${legacyJavaScriptResults.size} files.
496
+
497
+ Legacy JavaScript by file:
498
+ ${filesFormatted}`;
499
+ }
500
+ /**
501
+ * Create an AI prompt string out of the Modern HTTP Insight model to use with Ask AI.
502
+ * @param insight The Modern HTTP Model to query.
503
+ * @returns a string formatted for sending to Ask AI.
504
+ */
505
+ formatModernHttpInsight(insight) {
506
+ const requestSummary = (insight.http1Requests.length === 1) ?
507
+ TraceEventFormatter.networkRequests(insight.http1Requests, this.#parsedTrace, { verbose: true }) :
508
+ TraceEventFormatter.networkRequests(insight.http1Requests, this.#parsedTrace);
509
+ if (requestSummary.length === 0) {
510
+ return 'There are no requests that were served over a legacy HTTP protocol.';
511
+ }
512
+ return `Here is a list of the network requests that were served over a legacy HTTP protocol:
513
+ ${requestSummary}`;
514
+ }
300
515
  /**
301
516
  * Create an AI prompt string out of the NetworkDependencyTree Insight model to use with Ask AI.
302
517
  * Note: This function accesses the UIStrings within NetworkDependencyTree to help build the
@@ -368,6 +583,20 @@ export class PerformanceInsightFormatter {
368
583
  }
369
584
  return output;
370
585
  }
586
+ /**
587
+ * Create an AI prompt string out of the Render Blocking Insight model to use with Ask AI.
588
+ * @param insight The Render Blocking Model to query.
589
+ * @returns a string formatted for sending to Ask AI.
590
+ */
591
+ formatRenderBlockingInsight(insight) {
592
+ const requestSummary = TraceEventFormatter.networkRequests(insight.renderBlockingRequests, this.#parsedTrace);
593
+ if (requestSummary.length === 0) {
594
+ return 'There are no network requests that are render blocking.';
595
+ }
596
+ return `Here is a list of the network requests that were render blocking on this page and their duration:
597
+
598
+ ${requestSummary}`;
599
+ }
371
600
  /**
372
601
  * Create an AI prompt string out of the Slow CSS Selector Insight model to use with Ask AI.
373
602
  * Note: This function accesses the UIStrings within SlowCSSSelector to help build the
@@ -482,200 +711,51 @@ ${header} External resources:
482
711
  ${this.#links()}`;
483
712
  }
484
713
  #details() {
485
- if (Trace.Insights.Models.ImageDelivery.isImageDelivery(this.#insight)) {
486
- const optimizableImages = this.#insight.optimizableImages;
487
- if (optimizableImages.length === 0) {
488
- return 'There are no unoptimized images on this page.';
489
- }
490
- const imageDetails = optimizableImages
491
- .map(image => {
492
- // List potential optimizations for the image
493
- const optimizations = image.optimizations
494
- .map(optimization => {
495
- const message = Trace.Insights.Models.ImageDelivery.getOptimizationMessage(optimization);
496
- const byteSavings = bytes(optimization.byteSavings);
497
- return `${message} (Est ${byteSavings})`;
498
- })
499
- .join('\n');
500
- return `### ${image.request.args.data.url}
501
- - Potential savings: ${bytes(image.byteSavings)}
502
- - Optimizations:\n${optimizations}`;
503
- })
504
- .join('\n\n');
505
- return `Total potential savings: ${bytes(this.#insight.wastedBytes)}
506
-
507
- The following images could be optimized:\n\n${imageDetails}`;
508
- }
509
- if (Trace.Insights.Models.LCPBreakdown.isLCPBreakdown(this.#insight)) {
510
- const { subparts, lcpMs } = this.#insight;
511
- if (!lcpMs || !subparts) {
512
- return '';
513
- }
514
- // Text based LCP has TTFB & Render delay
515
- // Image based has TTFB, Load delay, Load time and Render delay
516
- // Note that we expect every trace + LCP to have TTFB + Render delay, but
517
- // very old traces are missing the data, so we have to code defensively
518
- // in case the subparts are not present.
519
- const phaseBulletPoints = [];
520
- Object.values(subparts).forEach((subpart) => {
521
- const phaseMilli = Trace.Helpers.Timing.microToMilli(subpart.range);
522
- const percentage = (phaseMilli / lcpMs * 100).toFixed(1);
523
- phaseBulletPoints.push({ name: subpart.label, value: this.#formatMilli(phaseMilli), percentage });
524
- });
525
- return `${this.#lcpMetricSharedContext()}
526
-
527
- We can break this time down into the ${phaseBulletPoints.length} phases that combine to make the LCP time:
528
-
529
- ${phaseBulletPoints.map(phase => `- ${phase.name}: ${phase.value} (${phase.percentage}% of total LCP time)`)
530
- .join('\n')}`;
714
+ if (Trace.Insights.Models.Cache.isCacheInsight(this.#insight)) {
715
+ return this.formatCacheInsight(this.#insight);
531
716
  }
532
- if (Trace.Insights.Models.LCPDiscovery.isLCPDiscovery(this.#insight)) {
533
- const { checklist, lcpEvent, lcpRequest, earliestDiscoveryTimeTs } = this.#insight;
534
- if (!checklist || !lcpEvent || !lcpRequest || !earliestDiscoveryTimeTs) {
535
- return '';
536
- }
537
- const checklistBulletPoints = [];
538
- checklistBulletPoints.push({
539
- name: checklist.priorityHinted.label,
540
- passed: checklist.priorityHinted.value,
541
- });
542
- checklistBulletPoints.push({
543
- name: checklist.eagerlyLoaded.label,
544
- passed: checklist.eagerlyLoaded.value,
545
- });
546
- checklistBulletPoints.push({
547
- name: checklist.requestDiscoverable.label,
548
- passed: checklist.requestDiscoverable.value,
549
- });
550
- return `${this.#lcpMetricSharedContext()}
551
-
552
- The result of the checks for this insight are:
553
- ${checklistBulletPoints.map(point => `- ${point.name}: ${point.passed ? 'PASSED' : 'FAILED'}`).join('\n')}`;
717
+ if (Trace.Insights.Models.CLSCulprits.isCLSCulpritsInsight(this.#insight)) {
718
+ return this.formatClsCulpritsInsight(this.#insight);
554
719
  }
555
- if (Trace.Insights.Models.RenderBlocking.isRenderBlocking(this.#insight)) {
556
- const requestSummary = TraceEventFormatter.networkRequests(this.#insight.renderBlockingRequests, this.#parsedTrace);
557
- if (requestSummary.length === 0) {
558
- return 'There are no network requests that are render blocking.';
559
- }
560
- return `Here is a list of the network requests that were render blocking on this page and their duration:
561
-
562
- ${requestSummary}`;
720
+ if (Trace.Insights.Models.DocumentLatency.isDocumentLatencyInsight(this.#insight)) {
721
+ return this.formatDocumentLatencyInsight(this.#insight);
563
722
  }
564
- if (Trace.Insights.Models.DocumentLatency.isDocumentLatency(this.#insight)) {
565
- if (!this.#insight.data) {
566
- return '';
567
- }
568
- const { checklist, documentRequest } = this.#insight.data;
569
- if (!documentRequest) {
570
- return '';
571
- }
572
- const checklistBulletPoints = [];
573
- checklistBulletPoints.push({
574
- name: 'The request was not redirected',
575
- passed: checklist.noRedirects.value,
576
- });
577
- checklistBulletPoints.push({
578
- name: 'Server responded quickly',
579
- passed: checklist.serverResponseIsFast.value,
580
- });
581
- checklistBulletPoints.push({
582
- name: 'Compression was applied',
583
- passed: checklist.usesCompression.value,
584
- });
585
- return `${this.#lcpMetricSharedContext()}
586
-
587
- ${TraceEventFormatter.networkRequests([documentRequest], this.#parsedTrace, {
588
- verbose: true,
589
- customTitle: 'Document network request'
590
- })}
591
-
592
- The result of the checks for this insight are:
593
- ${checklistBulletPoints.map(point => `- ${point.name}: ${point.passed ? 'PASSED' : 'FAILED'}`).join('\n')}`;
723
+ if (Trace.Insights.Models.DOMSize.isDomSizeInsight(this.#insight)) {
724
+ return this.formatDomSizeInsight(this.#insight);
594
725
  }
595
- if (Trace.Insights.Models.INPBreakdown.isINPBreakdown(this.#insight)) {
596
- const event = this.#insight.longestInteractionEvent;
597
- if (!event) {
598
- return '';
599
- }
600
- const inpInfoForEvent = `The longest interaction on the page was a \`${event.type}\` which had a total duration of \`${this.#formatMicro(event.dur)}\`. The timings of each of the three phases were:
601
-
602
- 1. Input delay: ${this.#formatMicro(event.inputDelay)}
603
- 2. Processing duration: ${this.#formatMicro(event.mainThreadHandling)}
604
- 3. Presentation delay: ${this.#formatMicro(event.presentationDelay)}.`;
605
- return inpInfoForEvent;
726
+ if (Trace.Insights.Models.DuplicatedJavaScript.isDuplicatedJavaScriptInsight(this.#insight)) {
727
+ return this.formatDuplicatedJavaScriptInsight(this.#insight);
606
728
  }
607
- if (Trace.Insights.Models.CLSCulprits.isCLSCulprits(this.#insight)) {
608
- const { worstCluster, shifts } = this.#insight;
609
- if (!worstCluster) {
610
- return '';
611
- }
612
- const baseTime = this.#parsedTrace.data.Meta.traceBounds.min;
613
- const clusterTimes = {
614
- start: worstCluster.ts - baseTime,
615
- end: worstCluster.ts + worstCluster.dur - baseTime,
616
- };
617
- const shiftsFormatted = worstCluster.events.map((layoutShift, index) => {
618
- return TraceEventFormatter.layoutShift(layoutShift, index, this.#parsedTrace, shifts.get(layoutShift));
619
- });
620
- return `The worst layout shift cluster was the cluster that started at ${this.#formatMicro(clusterTimes.start)} and ended at ${this.#formatMicro(clusterTimes.end)}, with a duration of ${this.#formatMicro(worstCluster.dur)}.
621
- The score for this cluster is ${worstCluster.clusterCumulativeScore.toFixed(4)}.
622
-
623
- Layout shifts in this cluster:
624
- ${shiftsFormatted.join('\n')}`;
729
+ if (Trace.Insights.Models.FontDisplay.isFontDisplayInsight(this.#insight)) {
730
+ return this.formatFontDisplayInsight(this.#insight);
625
731
  }
626
- if (Trace.Insights.Models.ModernHTTP.isModernHTTP(this.#insight)) {
627
- const requestSummary = (this.#insight.http1Requests.length === 1) ?
628
- TraceEventFormatter.networkRequests(this.#insight.http1Requests, this.#parsedTrace, { verbose: true }) :
629
- TraceEventFormatter.networkRequests(this.#insight.http1Requests, this.#parsedTrace);
630
- if (requestSummary.length === 0) {
631
- return 'There are no requests that were served over a legacy HTTP protocol.';
632
- }
633
- return `Here is a list of the network requests that were served over a legacy HTTP protocol:
634
- ${requestSummary}`;
732
+ if (Trace.Insights.Models.ForcedReflow.isForcedReflowInsight(this.#insight)) {
733
+ return this.formatForcedReflowInsight(this.#insight);
635
734
  }
636
- if (Trace.Insights.Models.DuplicatedJavaScript.isDuplicatedJavaScript(this.#insight)) {
637
- const totalWastedBytes = this.#insight.wastedBytes;
638
- const duplicatedScriptsByModule = this.#insight.duplicationGroupedByNodeModules;
639
- if (duplicatedScriptsByModule.size === 0) {
640
- return 'There is no duplicated JavaScript in the page modules';
641
- }
642
- const filesFormatted = Array.from(duplicatedScriptsByModule)
643
- .map(([module, duplication]) => `- Source: ${module} - Duplicated bytes: ${duplication.estimatedDuplicateBytes} bytes`)
644
- .join('\n');
645
- return `Total wasted bytes: ${totalWastedBytes} bytes.
646
-
647
- Duplication grouped by Node modules: ${filesFormatted}`;
735
+ if (Trace.Insights.Models.ImageDelivery.isImageDeliveryInsight(this.#insight)) {
736
+ return this.formatImageDeliveryInsight(this.#insight);
648
737
  }
649
- if (Trace.Insights.Models.LegacyJavaScript.isLegacyJavaScript(this.#insight)) {
650
- const legacyJavaScriptResults = this.#insight.legacyJavaScriptResults;
651
- if (legacyJavaScriptResults.size === 0) {
652
- return 'There is no significant amount of legacy JavaScript on the page.';
653
- }
654
- const filesFormatted = Array.from(legacyJavaScriptResults)
655
- .map(([script, result]) => `\n- Script: ${script.url} - Wasted bytes: ${result.estimatedByteSavings} bytes
656
- Matches:
657
- ${result.matches.map(match => `Line: ${match.line}, Column: ${match.column}, Name: ${match.name}`).join('\n')}`)
658
- .join('\n');
659
- return `Total legacy JavaScript: ${legacyJavaScriptResults.size} files.
660
-
661
- Legacy JavaScript by file:
662
- ${filesFormatted}`;
738
+ if (Trace.Insights.Models.INPBreakdown.isINPBreakdownInsight(this.#insight)) {
739
+ return this.formatInpBreakdownInsight(this.#insight);
663
740
  }
664
- if (Trace.Insights.Models.Cache.isCacheInsight(this.#insight)) {
665
- return this.formatCacheInsight(this.#insight);
741
+ if (Trace.Insights.Models.LCPBreakdown.isLCPBreakdownInsight(this.#insight)) {
742
+ return this.formatLcpBreakdownInsight(this.#insight);
666
743
  }
667
- if (Trace.Insights.Models.DOMSize.isDomSizeInsight(this.#insight)) {
668
- return this.formatDomSizeInsight(this.#insight);
744
+ if (Trace.Insights.Models.LCPDiscovery.isLCPDiscoveryInsight(this.#insight)) {
745
+ return this.formatLcpDiscoveryInsight(this.#insight);
669
746
  }
670
- if (Trace.Insights.Models.FontDisplay.isFontDisplayInsight(this.#insight)) {
671
- return this.formatFontDisplayInsight(this.#insight);
747
+ if (Trace.Insights.Models.LegacyJavaScript.isLegacyJavaScript(this.#insight)) {
748
+ return this.formatLegacyJavaScriptInsight(this.#insight);
672
749
  }
673
- if (Trace.Insights.Models.ForcedReflow.isForcedReflowInsight(this.#insight)) {
674
- return this.formatForcedReflowInsight(this.#insight);
750
+ if (Trace.Insights.Models.ModernHTTP.isModernHTTPInsight(this.#insight)) {
751
+ return this.formatModernHttpInsight(this.#insight);
675
752
  }
676
- if (Trace.Insights.Models.NetworkDependencyTree.isNetworkDependencyTree(this.#insight)) {
753
+ if (Trace.Insights.Models.NetworkDependencyTree.isNetworkDependencyTreeInsight(this.#insight)) {
677
754
  return this.formatNetworkDependencyTreeInsight(this.#insight);
678
755
  }
756
+ if (Trace.Insights.Models.RenderBlocking.isRenderBlockingInsight(this.#insight)) {
757
+ return this.formatRenderBlockingInsight(this.#insight);
758
+ }
679
759
  if (Trace.Insights.Models.SlowCSSSelector.isSlowCSSSelectorInsight(this.#insight)) {
680
760
  return this.formatSlowCssSelectorsInsight(this.#insight);
681
761
  }
@@ -9,7 +9,7 @@ const defaultTimeFormatterOptions = {
9
9
  style: 'unit',
10
10
  unitDisplay: 'narrow',
11
11
  minimumFractionDigits: 0,
12
- maximumFractionDigits: 1,
12
+ maximumFractionDigits: 0,
13
13
  };
14
14
  const defaultByteFormatterOptions = {
15
15
  style: 'unit',
@@ -22,8 +22,14 @@ const timeFormatters = {
22
22
  ...defaultTimeFormatterOptions,
23
23
  unit: 'millisecond',
24
24
  }),
25
+ milliWithPrecision: new Intl.NumberFormat('en-US', {
26
+ ...defaultTimeFormatterOptions,
27
+ maximumFractionDigits: 1,
28
+ unit: 'millisecond',
29
+ }),
25
30
  second: new Intl.NumberFormat('en-US', {
26
31
  ...defaultTimeFormatterOptions,
32
+ maximumFractionDigits: 1,
27
33
  unit: 'second',
28
34
  }),
29
35
  micro: new Intl.NumberFormat('en-US', {
@@ -71,6 +77,9 @@ export function millis(x) {
71
77
  if (numberIsTooLarge(x)) {
72
78
  return '-';
73
79
  }
80
+ if (x < 1) {
81
+ return formatAndEnsureSpace(timeFormatters.milliWithPrecision, x);
82
+ }
74
83
  return formatAndEnsureSpace(timeFormatters.milli, x);
75
84
  }
76
85
  export function micros(x) {
@@ -15,9 +15,10 @@ export class AgentFocus {
15
15
  }
16
16
  const insightSet = getFirstInsightSet(parsedTrace.insights);
17
17
  return new AgentFocus({
18
- type: 'full',
19
18
  parsedTrace,
20
19
  insightSet,
20
+ callTree: null,
21
+ insight: null,
21
22
  });
22
23
  }
23
24
  static fromInsight(parsedTrace, insight) {
@@ -26,9 +27,9 @@ export class AgentFocus {
26
27
  }
27
28
  const insightSet = getFirstInsightSet(parsedTrace.insights);
28
29
  return new AgentFocus({
29
- type: 'insight',
30
30
  parsedTrace,
31
31
  insightSet,
32
+ callTree: null,
32
33
  insight,
33
34
  });
34
35
  }
@@ -45,7 +46,7 @@ export class AgentFocus {
45
46
  })) ??
46
47
  getFirstInsightSet(insights);
47
48
  }
48
- return new AgentFocus({ type: 'call-tree', parsedTrace: callTree.parsedTrace, insightSet, callTree });
49
+ return new AgentFocus({ parsedTrace: callTree.parsedTrace, insightSet, callTree, insight: null });
49
50
  }
50
51
  #data;
51
52
  constructor(data) {
@@ -54,6 +55,16 @@ export class AgentFocus {
54
55
  get data() {
55
56
  return this.#data;
56
57
  }
58
+ withInsight(insight) {
59
+ const focus = new AgentFocus(this.#data);
60
+ focus.#data.insight = insight;
61
+ return focus;
62
+ }
63
+ withCallTree(callTree) {
64
+ const focus = new AgentFocus(this.#data);
65
+ focus.#data.callTree = callTree;
66
+ return focus;
67
+ }
57
68
  }
58
69
  export function getPerformanceAgentFocusFromModel(model) {
59
70
  const parsedTrace = model.parsedTrace();
@@ -85,9 +85,9 @@ export class ContentProviderBasedProject extends Workspace.Workspace.ProjectStor
85
85
  }
86
86
  async findFilesMatchingSearchRequest(searchConfig, filesMatchingFileQuery, progress) {
87
87
  const result = new Map();
88
- progress.setTotalWork(filesMatchingFileQuery.length);
88
+ progress.totalWork = filesMatchingFileQuery.length;
89
89
  await Promise.all(filesMatchingFileQuery.map(searchInContent.bind(this)));
90
- progress.done();
90
+ progress.done = true;
91
91
  return result;
92
92
  async function searchInContent(uiSourceCode) {
93
93
  let allMatchesFound = true;
@@ -103,11 +103,13 @@ export class ContentProviderBasedProject extends Workspace.Workspace.ProjectStor
103
103
  if (allMatchesFound) {
104
104
  result.set(uiSourceCode, matches);
105
105
  }
106
- progress.incrementWorked(1);
106
+ ++progress.worked;
107
107
  }
108
108
  }
109
109
  indexContent(progress) {
110
- queueMicrotask(progress.done.bind(progress));
110
+ queueMicrotask(() => {
111
+ progress.done = true;
112
+ });
111
113
  }
112
114
  addUISourceCodeWithProvider(uiSourceCode, contentProvider, metadata, mimeType) {
113
115
  this.#uiSourceCodeToData.set(uiSourceCode, { mimeType, metadata, contentProvider });
@@ -2,7 +2,6 @@
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 Common from '../../core/common/common.js';
5
- const MAX_WORKERS = Math.max(2, navigator.hardwareConcurrency - 1);
6
5
  let formatterWorkerPoolInstance;
7
6
  export class FormatterWorkerPool {
8
7
  taskQueue;
@@ -24,11 +23,12 @@ export class FormatterWorkerPool {
24
23
  return worker;
25
24
  }
26
25
  processNextTask() {
26
+ const maxWorkers = Math.max(2, navigator.hardwareConcurrency - 1);
27
27
  if (!this.taskQueue.length) {
28
28
  return;
29
29
  }
30
30
  let freeWorker = [...this.workerTasks.keys()].find(worker => !this.workerTasks.get(worker));
31
- if (!freeWorker && this.workerTasks.size < MAX_WORKERS) {
31
+ if (!freeWorker && this.workerTasks.size < maxWorkers) {
32
32
  freeWorker = this.createWorker();
33
33
  }
34
34
  if (!freeWorker) {
@@ -6,6 +6,9 @@ import * as Handlers from './handlers/handlers.js';
6
6
  import * as Helpers from './helpers/helpers.js';
7
7
  import { TraceParseProgressEvent, TraceProcessor } from './Processor.js';
8
8
  import * as Types from './types/types.js';
9
+ // Note: this model is implemented in a way that can support multiple trace
10
+ // processors. Currently there is only one implemented, but you will see
11
+ // references to "processors" plural because it can easily be extended in the future.
9
12
  /**
10
13
  * The Model is responsible for parsing arrays of raw trace events and storing the
11
14
  * resulting data. It can store multiple traces at once, and can return the data for
@@ -69,8 +72,6 @@ export class Model extends EventTarget {
69
72
  **/
70
73
  async parse(traceEvents, config) {
71
74
  const metadata = config?.metadata || {};
72
- const isFreshRecording = config?.isFreshRecording || false;
73
- const isCPUProfile = metadata?.dataOrigin === "CPUProfile" /* Types.File.DataOrigin.CPU_PROFILE */;
74
75
  // During parsing, periodically update any listeners on each processors'
75
76
  // progress (if they have any updates).
76
77
  const onTraceUpdate = (event) => {
@@ -83,13 +84,7 @@ export class Model extends EventTarget {
83
84
  try {
84
85
  // Wait for all outstanding promises before finishing the async execution,
85
86
  // but perform all tasks in parallel.
86
- const parseConfig = {
87
- isFreshRecording,
88
- isCPUProfile,
89
- metadata,
90
- resolveSourceMap: config?.resolveSourceMap,
91
- };
92
- await this.#processor.parse(traceEvents, parseConfig);
87
+ await this.#processor.parse(traceEvents, config ?? {});
93
88
  if (!this.#processor.data) {
94
89
  throw new Error('processor did not parse trace');
95
90
  }