chrome-devtools-frontend 1.0.1515988 → 1.0.1518653

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 (122) hide show
  1. package/docs/checklist/README.md +2 -2
  2. package/docs/checklist/javascript.md +1 -1
  3. package/docs/contributing/README.md +1 -1
  4. package/docs/contributing/settings-experiments-features.md +9 -8
  5. package/docs/cookbook/devtools_on_devtools.md +2 -2
  6. package/docs/cookbook/localization.md +10 -10
  7. package/docs/devtools-protocol.md +9 -8
  8. package/docs/ecosystem/automatic_workspace_folders.md +3 -3
  9. package/docs/get_the_code.md +0 -2
  10. package/docs/styleguide/ux/components.md +166 -85
  11. package/docs/styleguide/ux/numbers.md +3 -4
  12. package/front_end/core/common/README.md +13 -12
  13. package/front_end/core/host/GdpClient.ts +16 -1
  14. package/front_end/core/host/UserMetrics.ts +8 -2
  15. package/front_end/core/root/Runtime.ts +13 -0
  16. package/front_end/core/sdk/CSSMatchedStyles.ts +5 -1
  17. package/front_end/entrypoints/main/MainImpl.ts +6 -3
  18. package/front_end/generated/InspectorBackendCommands.js +10 -7
  19. package/front_end/generated/SupportedCSSProperties.js +21 -7
  20. package/front_end/generated/protocol-mapping.d.ts +16 -1
  21. package/front_end/generated/protocol-proxy-api.d.ts +13 -1
  22. package/front_end/generated/protocol.ts +95 -0
  23. package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +170 -54
  24. package/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.snapshot.txt +14 -181
  25. package/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.ts +13 -315
  26. package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.snapshot.txt +224 -50
  27. package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.ts +310 -11
  28. package/front_end/models/ai_assistance/performance/AIContext.ts +15 -2
  29. package/front_end/models/ai_code_completion/AiCodeCompletion.ts +41 -19
  30. package/front_end/models/badges/Badge.ts +8 -3
  31. package/front_end/models/badges/CodeWhispererBadge.ts +2 -4
  32. package/front_end/models/badges/StarterBadge.ts +2 -2
  33. package/front_end/models/badges/UserBadges.ts +59 -6
  34. package/front_end/models/formatter/FormatterWorkerPool.ts +3 -3
  35. package/front_end/models/javascript_metadata/NativeFunctions.js +1 -1
  36. package/front_end/models/trace/README.md +28 -1
  37. package/front_end/models/trace/handlers/UserTimingsHandler.ts +1 -1
  38. package/front_end/models/trace/helpers/Trace.ts +99 -43
  39. package/front_end/models/trace/types/TraceEvents.ts +9 -0
  40. package/front_end/panels/accessibility/ARIAAttributesView.ts +113 -191
  41. package/front_end/panels/accessibility/AccessibilityNodeView.ts +9 -9
  42. package/front_end/panels/accessibility/AccessibilitySubPane.ts +6 -4
  43. package/front_end/panels/accessibility/accessibilityProperties.css +2 -0
  44. package/front_end/panels/ai_assistance/AiAssistancePanel.ts +16 -2
  45. package/front_end/panels/ai_assistance/components/ChatView.ts +9 -10
  46. package/front_end/panels/ai_assistance/components/PerformanceAgentMarkdownRenderer.ts +42 -0
  47. package/front_end/panels/common/AiCodeCompletionDisclaimer.ts +32 -9
  48. package/front_end/panels/common/AiCodeCompletionSummaryToolbar.ts +7 -1
  49. package/front_end/panels/common/BadgeNotification.ts +67 -15
  50. package/front_end/panels/common/GdpSignUpDialog.ts +18 -9
  51. package/front_end/panels/console/ConsolePrompt.ts +1 -1
  52. package/front_end/panels/console/ConsoleView.ts +6 -2
  53. package/front_end/panels/elements/ComputedStyleWidget.ts +1 -2
  54. package/front_end/panels/elements/ElementsPanel.ts +4 -0
  55. package/front_end/panels/elements/ElementsTreeElement.ts +18 -0
  56. package/front_end/panels/elements/ElementsTreeOutline.ts +13 -0
  57. package/front_end/panels/elements/LayoutPane.ts +1 -1
  58. package/front_end/panels/elements/StylePropertyTreeElement.ts +21 -6
  59. package/front_end/panels/media/TickingFlameChart.ts +1 -1
  60. package/front_end/panels/network/NetworkLogView.ts +5 -1
  61. package/front_end/panels/profiler/HeapSnapshotView.ts +34 -19
  62. package/front_end/panels/search/SearchResultsPane.ts +126 -145
  63. package/front_end/panels/search/SearchView.ts +43 -59
  64. package/front_end/panels/settings/components/SyncSection.ts +16 -8
  65. package/front_end/panels/sources/AiCodeCompletionPlugin.ts +6 -1
  66. package/front_end/panels/sources/OutlineQuickOpen.ts +3 -1
  67. package/front_end/panels/sources/SourcesPanel.ts +3 -0
  68. package/front_end/panels/timeline/AppenderUtils.ts +2 -2
  69. package/front_end/panels/timeline/ExtensionTrackAppender.ts +13 -4
  70. package/front_end/panels/timeline/GPUTrackAppender.ts +2 -1
  71. package/front_end/panels/timeline/InteractionsTrackAppender.ts +5 -1
  72. package/front_end/panels/timeline/LayoutShiftsTrackAppender.ts +2 -1
  73. package/front_end/panels/timeline/ThreadAppender.ts +12 -3
  74. package/front_end/panels/timeline/TimelineFlameChartDataProvider.ts +9 -4
  75. package/front_end/panels/timeline/TimelinePanel.ts +3 -2
  76. package/front_end/panels/timeline/TimelineUIUtils.ts +18 -12
  77. package/front_end/panels/timeline/TimingsTrackAppender.ts +6 -1
  78. package/front_end/panels/timeline/components/CPUThrottlingSelector.ts +95 -82
  79. package/front_end/panels/timeline/components/LiveMetricsView.ts +2 -2
  80. package/front_end/panels/timeline/components/cpuThrottlingSelector.css +17 -15
  81. package/front_end/panels/timeline/components/insights/BaseInsightComponent.ts +3 -0
  82. package/front_end/third_party/chromium/README.chromium +1 -1
  83. package/front_end/third_party/codemirror.next/chunk/codemirror.js +1 -1
  84. package/front_end/third_party/codemirror.next/chunk/codemirror.js.map +1 -1
  85. package/front_end/third_party/codemirror.next/codemirror.next.d.ts +6 -9
  86. package/front_end/third_party/codemirror.next/package.json +2 -1
  87. package/front_end/third_party/diff/README.chromium +1 -0
  88. package/front_end/third_party/puppeteer/README.chromium +2 -2
  89. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/core/Realm.d.ts +2 -2
  90. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/cdp/Accessibility.js +0 -20
  91. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/cdp/Accessibility.js.map +1 -1
  92. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/generated/version.d.ts +1 -1
  93. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/generated/version.js +1 -1
  94. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/injected/injected.d.ts +1 -1
  95. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/revisions.d.ts +1 -1
  96. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/revisions.js +1 -1
  97. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/revisions.js.map +1 -1
  98. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/util/Mutex.d.ts +2 -2
  99. package/front_end/third_party/puppeteer/package/lib/es5-iife/puppeteer-core-browser.js +2 -23
  100. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/cdp/Accessibility.js +0 -20
  101. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/cdp/Accessibility.js.map +1 -1
  102. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/generated/version.d.ts +1 -1
  103. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/generated/version.js +1 -1
  104. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/revisions.d.ts +1 -1
  105. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/revisions.js +1 -1
  106. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/revisions.js.map +1 -1
  107. package/front_end/third_party/puppeteer/package/package.json +1 -1
  108. package/front_end/third_party/puppeteer/package/src/cdp/Accessibility.ts +1 -21
  109. package/front_end/third_party/puppeteer/package/src/generated/version.ts +1 -1
  110. package/front_end/third_party/puppeteer/package/src/revisions.ts +1 -1
  111. package/front_end/ui/components/text_editor/config.ts +36 -8
  112. package/front_end/ui/components/tooltips/Tooltip.ts +71 -34
  113. package/front_end/ui/legacy/README.md +33 -24
  114. package/front_end/ui/legacy/SearchableView.ts +19 -26
  115. package/front_end/ui/legacy/TextPrompt.ts +166 -1
  116. package/front_end/ui/legacy/Treeoutline.ts +16 -2
  117. package/front_end/ui/legacy/UIUtils.ts +15 -2
  118. package/front_end/ui/legacy/XElement.ts +0 -43
  119. package/front_end/ui/legacy/components/perf_ui/FlameChart.ts +20 -4
  120. package/front_end/ui/visual_logging/KnownContextValues.ts +24 -6
  121. package/front_end/ui/visual_logging/README.md +43 -27
  122. package/package.json +1 -1
@@ -8,22 +8,30 @@ import type {AICallTree} from '../performance/AICallTree.js';
8
8
  import type {AgentFocus} from '../performance/AIContext.js';
9
9
  import {AIQueries} from '../performance/AIQueries.js';
10
10
 
11
- import {PerformanceInsightFormatter, TraceEventFormatter} from './PerformanceInsightFormatter.js';
11
+ import {NetworkRequestFormatter} from './NetworkRequestFormatter.js';
12
+ import {PerformanceInsightFormatter} from './PerformanceInsightFormatter.js';
12
13
  import {bytes, micros, millis} from './UnitFormatters.js';
13
14
 
15
+ export interface NetworkRequestFormatOptions {
16
+ verbose?: boolean;
17
+ customTitle?: string;
18
+ }
19
+
14
20
  export class PerformanceTraceFormatter {
21
+ #focus: AgentFocus;
15
22
  #parsedTrace: Trace.TraceModel.ParsedTrace;
16
23
  #insightSet: Trace.Insights.Types.InsightSet|null;
17
- #eventsSerializer: Trace.EventsSerializer.EventsSerializer;
24
+ protected eventsSerializer: Trace.EventsSerializer.EventsSerializer;
18
25
 
19
- constructor(focus: AgentFocus, eventsSerializer: Trace.EventsSerializer.EventsSerializer) {
26
+ constructor(focus: AgentFocus) {
27
+ this.#focus = focus;
20
28
  this.#parsedTrace = focus.data.parsedTrace;
21
29
  this.#insightSet = focus.data.insightSet;
22
- this.#eventsSerializer = eventsSerializer;
30
+ this.eventsSerializer = focus.eventsSerializer;
23
31
  }
24
32
 
25
33
  serializeEvent(event: Trace.Types.Events.Event): string {
26
- const key = this.#eventsSerializer.keyForEvent(event);
34
+ const key = this.eventsSerializer.keyForEvent(event);
27
35
  return `(eventKey: ${key}, ts: ${event.ts})`;
28
36
  }
29
37
 
@@ -167,7 +175,7 @@ export class PerformanceTraceFormatter {
167
175
  continue;
168
176
  }
169
177
 
170
- const formatter = new PerformanceInsightFormatter(parsedTrace, model);
178
+ const formatter = new PerformanceInsightFormatter(this.#focus, model);
171
179
  if (!formatter.insightIsSupported()) {
172
180
  continue;
173
181
  }
@@ -199,7 +207,6 @@ export class PerformanceTraceFormatter {
199
207
  }
200
208
 
201
209
  formatCriticalRequests(): string {
202
- const parsedTrace = this.#parsedTrace;
203
210
  const insightSet = this.#insightSet;
204
211
  const criticalRequests: Trace.Types.Events.SyntheticNetworkRequest[] = [];
205
212
 
@@ -213,8 +220,7 @@ export class PerformanceTraceFormatter {
213
220
  return '';
214
221
  }
215
222
 
216
- return 'Critical network requests:\n' +
217
- TraceEventFormatter.networkRequests(criticalRequests, parsedTrace, {verbose: false});
223
+ return 'Critical network requests:\n' + this.formatNetworkRequests(criticalRequests, {verbose: false});
218
224
  }
219
225
 
220
226
  #serializeBottomUpRootNode(rootNode: Trace.Extras.TraceTree.BottomUpRootNode, limit: number): string {
@@ -421,7 +427,7 @@ export class PerformanceTraceFormatter {
421
427
 
422
428
  const requests = this.#parsedTrace.data.NetworkRequests.byTime.filter(
423
429
  request => Trace.Helpers.Timing.eventIsInBounds(request, bounds));
424
- const requestsText = TraceEventFormatter.networkRequests(requests, this.#parsedTrace, {verbose: false});
430
+ const requestsText = this.formatNetworkRequests(requests, {verbose: false});
425
431
  results.push('# Network requests summary');
426
432
  results.push(requestsText || 'No requests in the given bounds');
427
433
 
@@ -443,11 +449,304 @@ export class PerformanceTraceFormatter {
443
449
  results.push('#'.repeat(headerLevel) + ' Node id to eventKey\n');
444
450
  results.push('These node ids correspond to the call tree nodes listed in the above section.\n');
445
451
  tree.breadthFirstWalk(tree.rootNode.children().values(), (node, nodeId) => {
446
- results.push(`${nodeId}: ${this.#eventsSerializer.keyForEvent(node.event)}`);
452
+ results.push(`${nodeId}: ${this.eventsSerializer.keyForEvent(node.event)}`);
447
453
  });
448
454
 
449
455
  results.push('\nIMPORTANT: Never show eventKey to the user.');
450
456
 
451
457
  return results.join('\n');
452
458
  }
459
+
460
+ formatNetworkRequests(
461
+ requests: readonly Trace.Types.Events.SyntheticNetworkRequest[], options?: NetworkRequestFormatOptions): string {
462
+ if (requests.length === 0) {
463
+ return '';
464
+ }
465
+
466
+ let verbose;
467
+ if (options?.verbose !== undefined) {
468
+ verbose = options.verbose;
469
+ } else {
470
+ verbose = requests.length === 1;
471
+ }
472
+
473
+ // Use verbose format for a single network request. With the compressed format, a format description
474
+ // needs to be provided, which is not worth sending if only one network request is being stringified.
475
+ if (verbose) {
476
+ return requests.map(request => this.#networkRequestVerbosely(request, options)).join('\n');
477
+ }
478
+
479
+ return this.#networkRequestsArrayCompressed(requests);
480
+ }
481
+
482
+ #getOrAssignUrlIndex(urlIdToIndex: Map<string, number>, url: string): number {
483
+ let index = urlIdToIndex.get(url);
484
+ if (index !== undefined) {
485
+ return index;
486
+ }
487
+ index = urlIdToIndex.size;
488
+ urlIdToIndex.set(url, index);
489
+ return index;
490
+ }
491
+
492
+ #getInitiatorChain(parsedTrace: Trace.TraceModel.ParsedTrace, request: Trace.Types.Events.SyntheticNetworkRequest):
493
+ Trace.Types.Events.SyntheticNetworkRequest[] {
494
+ const initiators: Trace.Types.Events.SyntheticNetworkRequest[] = [];
495
+
496
+ let cur: Trace.Types.Events.SyntheticNetworkRequest|undefined = request;
497
+ while (cur) {
498
+ const initiator = parsedTrace.data.NetworkRequests.eventToInitiator.get(cur);
499
+ if (initiator) {
500
+ // Should never happen, but if it did that would be an infinite loop.
501
+ if (initiators.includes(initiator)) {
502
+ return [];
503
+ }
504
+
505
+ initiators.unshift(initiator);
506
+ }
507
+
508
+ cur = initiator;
509
+ }
510
+
511
+ return initiators;
512
+ }
513
+
514
+ /**
515
+ * This is the data passed to a network request when the Performance Insights
516
+ * agent is asking for information. It is a slimmed down version of the
517
+ * request's data to avoid using up too much of the context window.
518
+ * IMPORTANT: these set of fields have been reviewed by Chrome Privacy &
519
+ * Security; be careful about adding new data here. If you are in doubt please
520
+ * talk to jacktfranklin@.
521
+ */
522
+ #networkRequestVerbosely(request: Trace.Types.Events.SyntheticNetworkRequest, options?: NetworkRequestFormatOptions):
523
+ string {
524
+ const {
525
+ url,
526
+ statusCode,
527
+ initialPriority,
528
+ priority,
529
+ fromServiceWorker,
530
+ mimeType,
531
+ responseHeaders,
532
+ syntheticData,
533
+ protocol
534
+ } = request.args.data;
535
+ const parsedTrace = this.#parsedTrace;
536
+
537
+ const titlePrefix = `## ${options?.customTitle ?? 'Network request'}`;
538
+
539
+ // Note: unlike other agents, we do have the ability to include
540
+ // cross-origins, hence why we do not sanitize the URLs here.
541
+ const navigationForEvent = Trace.Helpers.Trace.getNavigationForTraceEvent(
542
+ request,
543
+ request.args.data.frame,
544
+ parsedTrace.data.Meta.navigationsByFrameId,
545
+ );
546
+ const baseTime = navigationForEvent?.ts ?? parsedTrace.data.Meta.traceBounds.min;
547
+
548
+ // Gets all the timings for this request, relative to the base time.
549
+ // Note that this is the start time, not total time. E.g. "queuedAt: X"
550
+ // means that the request was queued at Xms, not that it queued for Xms.
551
+ const startTimesForLifecycle = {
552
+ queuedAt: request.ts - baseTime,
553
+ requestSentAt: syntheticData.sendStartTime - baseTime,
554
+ downloadCompletedAt: syntheticData.finishTime - baseTime,
555
+ processingCompletedAt: request.ts + request.dur - baseTime,
556
+ } as const;
557
+
558
+ const mainThreadProcessingDuration =
559
+ startTimesForLifecycle.processingCompletedAt - startTimesForLifecycle.downloadCompletedAt;
560
+ const downloadTime = syntheticData.finishTime - syntheticData.downloadStart;
561
+
562
+ const renderBlocking = Trace.Helpers.Network.isSyntheticNetworkRequestEventRenderBlocking(request);
563
+ const initiator = parsedTrace.data.NetworkRequests.eventToInitiator.get(request);
564
+
565
+ const priorityLines = [];
566
+ if (initialPriority === priority) {
567
+ priorityLines.push(`Priority: ${priority}`);
568
+ } else {
569
+ priorityLines.push(`Initial priority: ${initialPriority}`);
570
+ priorityLines.push(`Final priority: ${priority}`);
571
+ }
572
+
573
+ const redirects = request.args.data.redirects.map((redirect, index) => {
574
+ const startTime = redirect.ts - baseTime;
575
+ return `#### Redirect ${index + 1}: ${redirect.url}
576
+ - Start time: ${micros(startTime)}
577
+ - Duration: ${micros(redirect.dur)}`;
578
+ });
579
+
580
+ const initiators = this.#getInitiatorChain(parsedTrace, request);
581
+ const initiatorUrls = initiators.map(initiator => initiator.args.data.url);
582
+
583
+ const eventKey = this.eventsSerializer.keyForEvent(request);
584
+ const eventKeyLine = eventKey ? `eventKey: ${eventKey}\n` : '';
585
+
586
+ return `${titlePrefix}: ${url}
587
+ ${eventKeyLine}Timings:
588
+ - Queued at: ${micros(startTimesForLifecycle.queuedAt)}
589
+ - Request sent at: ${micros(startTimesForLifecycle.requestSentAt)}
590
+ - Download complete at: ${micros(startTimesForLifecycle.downloadCompletedAt)}
591
+ - Main thread processing completed at: ${micros(startTimesForLifecycle.processingCompletedAt)}
592
+ Durations:
593
+ - Download time: ${micros(downloadTime)}
594
+ - Main thread processing time: ${micros(mainThreadProcessingDuration)}
595
+ - Total duration: ${micros(request.dur)}${initiator ? `\nInitiator: ${initiator.args.data.url}` : ''}
596
+ Redirects:${redirects.length ? '\n' + redirects.join('\n') : ' no redirects'}
597
+ Status code: ${statusCode}
598
+ MIME Type: ${mimeType}
599
+ Protocol: ${protocol}
600
+ ${priorityLines.join('\n')}
601
+ Render blocking: ${renderBlocking ? 'Yes' : 'No'}
602
+ From a service worker: ${fromServiceWorker ? 'Yes' : 'No'}
603
+ Initiators (root request to the request that directly loaded this one): ${initiatorUrls.join(', ') || 'none'}
604
+ ${NetworkRequestFormatter.formatHeaders('Response headers', responseHeaders ?? [], true)}`;
605
+ }
606
+
607
+ // A compact network requests format designed to save tokens when sending multiple network requests to the model.
608
+ // It creates a map that maps request URLs to IDs and references the IDs in the compressed format.
609
+ //
610
+ // Important: Do not use this method for stringifying a single network request. With this format, a format description
611
+ // needs to be provided, which is not worth sending if only one network request is being stringified.
612
+ // For a single request, use `formatRequestVerbosely`, which formats with all fields specified and does not require a
613
+ // format description.
614
+ #networkRequestsArrayCompressed(requests: readonly Trace.Types.Events.SyntheticNetworkRequest[]): string {
615
+ const networkDataString = `
616
+ Network requests data:
617
+
618
+ `;
619
+ const urlIdToIndex = new Map<string, number>();
620
+ const allRequestsText = requests
621
+ .map(request => {
622
+ const urlIndex = this.#getOrAssignUrlIndex(urlIdToIndex, request.args.data.url);
623
+ return this.#networkRequestCompressedFormat(urlIndex, request, urlIdToIndex);
624
+ })
625
+ .join('\n');
626
+
627
+ const urlsMapString = 'allUrls = ' +
628
+ `[${
629
+ Array.from(urlIdToIndex.entries())
630
+ .map(([url, index]) => {
631
+ return `${index}: ${url}`;
632
+ })
633
+ .join(', ')}]`;
634
+
635
+ return networkDataString + '\n\n' + urlsMapString + '\n\n' + allRequestsText;
636
+ }
637
+
638
+ /**
639
+ * Network requests format description that is sent to the model as a fact.
640
+ */
641
+ static networkDataFormatDescription = `Network requests are formatted like this:
642
+ \`urlIndex;eventKey;queuedTime;requestSentTime;downloadCompleteTime;processingCompleteTime;totalDuration;downloadDuration;mainThreadProcessingDuration;statusCode;mimeType;priority;initialPriority;finalPriority;renderBlocking;protocol;fromServiceWorker;initiators;redirects:[[redirectUrlIndex|startTime|duration]];responseHeaders:[header1Value|header2Value|...]\`
643
+
644
+ - \`urlIndex\`: Numerical index for the request's URL, referencing the "All URLs" list.
645
+ - \`eventKey\`: String that uniquely identifies this request's trace event.
646
+ Timings (all in milliseconds, relative to navigation start):
647
+ - \`queuedTime\`: When the request was queued.
648
+ - \`requestSentTime\`: When the request was sent.
649
+ - \`downloadCompleteTime\`: When the download completed.
650
+ - \`processingCompleteTime\`: When main thread processing finished.
651
+ Durations (all in milliseconds):
652
+ - \`totalDuration\`: Total time from the request being queued until its main thread processing completed.
653
+ - \`downloadDuration\`: Time spent actively downloading the resource.
654
+ - \`mainThreadProcessingDuration\`: Time spent on the main thread after the download completed.
655
+ - \`statusCode\`: The HTTP status code of the response (e.g., 200, 404).
656
+ - \`mimeType\`: The MIME type of the resource (e.g., "text/html", "application/javascript").
657
+ - \`priority\`: The final network request priority (e.g., "VeryHigh", "Low").
658
+ - \`initialPriority\`: The initial network request priority.
659
+ - \`finalPriority\`: The final network request priority (redundant if \`priority\` is always final, but kept for clarity if \`initialPriority\` and \`priority\` differ).
660
+ - \`renderBlocking\`: 't' if the request was render-blocking, 'f' otherwise.
661
+ - \`protocol\`: The network protocol used (e.g., "h2", "http/1.1").
662
+ - \`fromServiceWorker\`: 't' if the request was served from a service worker, 'f' otherwise.
663
+ - \`initiators\`: A list (separated by ,) of URL indices for the initiator chain of this request. Listed in order starting from the root request to the request that directly loaded this one. This represents the network dependencies necessary to load this request. If there is no initiator, this is empty.
664
+ - \`redirects\`: A comma-separated list of redirects, enclosed in square brackets. Each redirect is formatted as
665
+ \`[redirectUrlIndex|startTime|duration]\`, where: \`redirectUrlIndex\`: Numerical index for the redirect's URL. \`startTime\`: The start time of the redirect in milliseconds, relative to navigation start. \`duration\`: The duration of the redirect in milliseconds.
666
+ - \`responseHeaders\`: A list (separated by '|') of values for specific, pre-defined response headers, enclosed in square brackets.
667
+ The order of headers corresponds to an internal fixed list. If a header is not present, its value will be empty.
668
+ `;
669
+
670
+ /**
671
+ * This is the network request data passed to the Performance agent.
672
+ *
673
+ * The `urlIdToIndex` Map is used to map URLs to numerical indices in order to not need to pass whole url every time it's mentioned.
674
+ * The map content is passed in the response together will all the requests data.
675
+ *
676
+ * See `networkDataFormatDescription` above for specifics.
677
+ */
678
+ #networkRequestCompressedFormat(
679
+ urlIndex: number, request: Trace.Types.Events.SyntheticNetworkRequest,
680
+ urlIdToIndex: Map<string, number>): string {
681
+ const {
682
+ statusCode,
683
+ initialPriority,
684
+ priority,
685
+ fromServiceWorker,
686
+ mimeType,
687
+ responseHeaders,
688
+ syntheticData,
689
+ protocol,
690
+ } = request.args.data;
691
+ const parsedTrace = this.#parsedTrace;
692
+
693
+ const navigationForEvent = Trace.Helpers.Trace.getNavigationForTraceEvent(
694
+ request,
695
+ request.args.data.frame,
696
+ parsedTrace.data.Meta.navigationsByFrameId,
697
+ );
698
+ const baseTime = navigationForEvent?.ts ?? parsedTrace.data.Meta.traceBounds.min;
699
+ const queuedTime = micros(request.ts - baseTime);
700
+ const requestSentTime = micros(syntheticData.sendStartTime - baseTime);
701
+ const downloadCompleteTime = micros(syntheticData.finishTime - baseTime);
702
+ const processingCompleteTime = micros(request.ts + request.dur - baseTime);
703
+ const totalDuration = micros(request.dur);
704
+ const downloadDuration = micros(syntheticData.finishTime - syntheticData.downloadStart);
705
+ const mainThreadProcessingDuration = micros(request.ts + request.dur - syntheticData.finishTime);
706
+ const renderBlocking = Trace.Helpers.Network.isSyntheticNetworkRequestEventRenderBlocking(request) ? 't' : 'f';
707
+ const finalPriority = priority;
708
+ const headerValues = responseHeaders
709
+ ?.map(header => {
710
+ const value =
711
+ NetworkRequestFormatter.allowHeader(header.name) ? header.value : '<redacted>';
712
+ return `${header.name}: ${value}`;
713
+ })
714
+ .join('|');
715
+ const redirects = request.args.data.redirects
716
+ .map(redirect => {
717
+ const urlIndex = this.#getOrAssignUrlIndex(urlIdToIndex, redirect.url);
718
+ const redirectStartTime = micros(redirect.ts - baseTime);
719
+ const redirectDuration = micros(redirect.dur);
720
+ return `[${urlIndex}|${redirectStartTime}|${redirectDuration}]`;
721
+ })
722
+ .join(',');
723
+
724
+ const initiators = this.#getInitiatorChain(parsedTrace, request);
725
+ const initiatorUrlIndices =
726
+ initiators.map(initiator => this.#getOrAssignUrlIndex(urlIdToIndex, initiator.args.data.url));
727
+
728
+ const parts = [
729
+ urlIndex,
730
+ this.eventsSerializer.keyForEvent(request) ?? '',
731
+ queuedTime,
732
+ requestSentTime,
733
+ downloadCompleteTime,
734
+ processingCompleteTime,
735
+ totalDuration,
736
+ downloadDuration,
737
+ mainThreadProcessingDuration,
738
+ statusCode,
739
+ mimeType,
740
+ priority,
741
+ initialPriority,
742
+ finalPriority,
743
+ renderBlocking,
744
+ protocol,
745
+ fromServiceWorker ? 't' : 'f',
746
+ initiatorUrlIndices.join(','),
747
+ `[${redirects}]`,
748
+ `[${headerValues ?? ''}]`,
749
+ ];
750
+ return parts.join(';');
751
+ }
453
752
  }
@@ -21,7 +21,7 @@ function getFirstInsightSet(insights: Trace.Insights.Types.TraceInsightSets): Tr
21
21
  }
22
22
 
23
23
  export class AgentFocus {
24
- static full(parsedTrace: Trace.TraceModel.ParsedTrace): AgentFocus {
24
+ static fromParsedTrace(parsedTrace: Trace.TraceModel.ParsedTrace): AgentFocus {
25
25
  if (!parsedTrace.insights) {
26
26
  throw new Error('missing insights');
27
27
  }
@@ -69,6 +69,7 @@ export class AgentFocus {
69
69
  }
70
70
 
71
71
  #data: AgentFocusData;
72
+ readonly eventsSerializer = new Trace.EventsSerializer.EventsSerializer();
72
73
 
73
74
  constructor(data: AgentFocusData) {
74
75
  this.#data = data;
@@ -89,6 +90,18 @@ export class AgentFocus {
89
90
  focus.#data.callTree = callTree;
90
91
  return focus;
91
92
  }
93
+
94
+ lookupEvent(key: Trace.Types.File.SerializableKey): Trace.Types.Events.Event|null {
95
+ try {
96
+ return this.eventsSerializer.eventForKey(key, this.#data.parsedTrace);
97
+ } catch (err) {
98
+ if (err.toString().includes('Unknown trace event')) {
99
+ return null;
100
+ }
101
+
102
+ throw err;
103
+ }
104
+ }
92
105
  }
93
106
 
94
107
  export function getPerformanceAgentFocusFromModel(model: Trace.TraceModel.Model): AgentFocus|null {
@@ -97,5 +110,5 @@ export function getPerformanceAgentFocusFromModel(model: Trace.TraceModel.Model)
97
110
  return null;
98
111
  }
99
112
 
100
- return AgentFocus.full(parsedTrace);
113
+ return AgentFocus.fromParsedTrace(parsedTrace);
101
114
  }
@@ -143,13 +143,14 @@ export class AiCodeCompletion extends Common.ObjectWrapper.ObjectWrapper<EventTy
143
143
  #stopSequences: string[];
144
144
  #renderingTimeout?: number;
145
145
  #aidaRequestCache?: CachedRequest;
146
- #panel: Panel;
146
+ #panel: ContextFlavor;
147
147
 
148
148
  readonly #sessionId: string = crypto.randomUUID();
149
149
  readonly #aidaClient: Host.AidaClient.AidaClient;
150
150
  readonly #serverSideLoggingEnabled: boolean;
151
151
 
152
- constructor(opts: AgentOptions, editor: TextEditor.TextEditor.TextEditor, panel: Panel, stopSequences?: string[]) {
152
+ constructor(
153
+ opts: AgentOptions, editor: TextEditor.TextEditor.TextEditor, panel: ContextFlavor, stopSequences?: string[]) {
153
154
  super();
154
155
  this.#aidaClient = opts.aidaClient;
155
156
  this.#serverSideLoggingEnabled = opts.serverSideLoggingEnabled ?? false;
@@ -159,8 +160,10 @@ export class AiCodeCompletion extends Common.ObjectWrapper.ObjectWrapper<EventTy
159
160
  }
160
161
 
161
162
  #debouncedRequestAidaSuggestion = Common.Debouncer.debounce(
162
- (prefix: string, suffix: string, cursor: number, inferenceLanguage?: Host.AidaClient.AidaInferenceLanguage) => {
163
- void this.#requestAidaSuggestion(this.#buildRequest(prefix, suffix, inferenceLanguage), cursor);
163
+ (prefix: string, suffix: string, cursorPositionAtRequest: number,
164
+ inferenceLanguage?: Host.AidaClient.AidaInferenceLanguage) => {
165
+ void this.#requestAidaSuggestion(
166
+ this.#buildRequest(prefix, suffix, inferenceLanguage), cursorPositionAtRequest);
164
167
  },
165
168
  AIDA_REQUEST_DEBOUNCE_TIMEOUT_MS);
166
169
 
@@ -175,12 +178,12 @@ export class AiCodeCompletion extends Common.ObjectWrapper.ObjectWrapper<EventTy
175
178
  // As a temporary fix for b/441221870 we are prepending a newline for each prefix.
176
179
  prefix = '\n' + prefix;
177
180
 
178
- const additionalFiles = this.#panel === Panel.CONSOLE ? [{
181
+ const additionalFiles = this.#panel === ContextFlavor.CONSOLE ? [{
179
182
  path: 'devtools-console-context.js',
180
183
  content: consoleAdditionalContextFileContent,
181
184
  included_reason: Host.AidaClient.Reason.RELATED_FILE,
182
185
  }] :
183
- undefined;
186
+ undefined;
184
187
 
185
188
  return {
186
189
  client: Host.AidaClient.CLIENT_NAME,
@@ -274,6 +277,11 @@ export class AiCodeCompletion extends Common.ObjectWrapper.ObjectWrapper<EventTy
274
277
  return null;
275
278
  }
276
279
 
280
+ const isRepetitive = this.#checkIfSuggestionRepeatsExistingText(suggestionSample.generationString, request);
281
+ if (isRepetitive) {
282
+ return null;
283
+ }
284
+
277
285
  const suggestionText = this.#trimSuggestionOverlap(suggestionSample.generationString, request);
278
286
  if (suggestionText.length === 0) {
279
287
  return null;
@@ -288,12 +296,15 @@ export class AiCodeCompletion extends Common.ObjectWrapper.ObjectWrapper<EventTy
288
296
  };
289
297
  }
290
298
 
291
- async #requestAidaSuggestion(request: Host.AidaClient.CompletionRequest, cursor: number): Promise<void> {
299
+ async #requestAidaSuggestion(request: Host.AidaClient.CompletionRequest, cursorPositionAtRequest: number):
300
+ Promise<void> {
292
301
  const startTime = performance.now();
293
302
  this.dispatchEventToListeners(Events.REQUEST_TRIGGERED, {});
303
+ // Registering AiCodeCompletionRequestTriggered metric even if the request is served from cache
304
+ Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiCodeCompletionRequestTriggered);
294
305
 
295
306
  try {
296
- const sampleResponse = await this.#generateSampleForRequest(request, cursor);
307
+ const sampleResponse = await this.#generateSampleForRequest(request, cursorPositionAtRequest);
297
308
  if (!sampleResponse) {
298
309
  this.dispatchEventToListeners(Events.RESPONSE_RECEIVED, {});
299
310
  return;
@@ -308,12 +319,19 @@ export class AiCodeCompletion extends Common.ObjectWrapper.ObjectWrapper<EventTy
308
319
  } = sampleResponse;
309
320
  const remainingDelay = Math.max(DELAY_BEFORE_SHOWING_RESPONSE_MS - (performance.now() - startTime), 0);
310
321
  this.#renderingTimeout = window.setTimeout(() => {
322
+ const currentCursorPosition = this.#editor.editor.state.selection.main.head;
323
+ if (currentCursorPosition !== cursorPositionAtRequest) {
324
+ this.dispatchEventToListeners(Events.RESPONSE_RECEIVED, {});
325
+ return;
326
+ }
311
327
  this.#editor.dispatch({
312
328
  effects: TextEditor.Config.setAiAutoCompleteSuggestion.of({
313
329
  text: suggestionText,
314
- from: cursor,
330
+ from: cursorPositionAtRequest,
315
331
  rpcGlobalId,
316
332
  sampleId,
333
+ startTime,
334
+ onImpression: this.#registerUserImpression.bind(this),
317
335
  })
318
336
  });
319
337
 
@@ -321,17 +339,13 @@ export class AiCodeCompletion extends Common.ObjectWrapper.ObjectWrapper<EventTy
321
339
  Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiCodeCompletionResponseServedFromCache);
322
340
  }
323
341
 
324
- if (rpcGlobalId) {
325
- const latency = performance.now() - startTime;
326
- this.#registerUserImpression(rpcGlobalId, sampleId, latency);
327
- }
328
-
329
- debugLog('Suggestion dispatched to the editor', suggestionText, 'at cursor position', cursor);
342
+ debugLog('Suggestion dispatched to the editor', suggestionText, 'at cursor position', cursorPositionAtRequest);
330
343
  this.dispatchEventToListeners(Events.RESPONSE_RECEIVED, {citations});
331
344
  }, remainingDelay);
332
345
  } catch (e) {
333
346
  debugLog('Error while fetching code completion suggestions from AIDA', e);
334
347
  this.dispatchEventToListeners(Events.RESPONSE_RECEIVED, {});
348
+ Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiCodeCompletionError);
335
349
  }
336
350
  }
337
351
 
@@ -368,6 +382,11 @@ export class AiCodeCompletion extends Common.ObjectWrapper.ObjectWrapper<EventTy
368
382
  return generationString;
369
383
  }
370
384
 
385
+ #checkIfSuggestionRepeatsExistingText(generationString: string, request: Host.AidaClient.CompletionRequest): boolean {
386
+ const {prefix, suffix} = request;
387
+ return Boolean(prefix.includes(generationString.trim()) || suffix?.includes(generationString.trim()));
388
+ }
389
+
371
390
  #checkCachedRequestForResponse(request: Host.AidaClient.CompletionRequest): Host.AidaClient.CompletionResponse|null {
372
391
  if (!this.#aidaRequestCache || this.#aidaRequestCache.request.suffix !== request.suffix ||
373
392
  JSON.stringify(this.#aidaRequestCache.request.options) !== JSON.stringify(request.options)) {
@@ -418,6 +437,7 @@ export class AiCodeCompletion extends Common.ObjectWrapper.ObjectWrapper<EventTy
418
437
  },
419
438
  });
420
439
  debugLog('Registered user impression with latency {seconds:', seconds, ', nanos:', nanos, '}');
440
+ Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiCodeCompletionSuggestionDisplayed);
421
441
  }
422
442
 
423
443
  registerUserAcceptance(rpcGlobalId: Host.AidaClient.RpcGlobalId, sampleId: number): void {
@@ -433,11 +453,13 @@ export class AiCodeCompletion extends Common.ObjectWrapper.ObjectWrapper<EventTy
433
453
  },
434
454
  });
435
455
  debugLog('Registered user acceptance');
456
+ Host.userMetrics.actionTaken(Host.UserMetrics.Action.AiCodeCompletionSuggestionAccepted);
436
457
  }
437
458
 
438
459
  onTextChanged(
439
- prefix: string, suffix: string, cursor: number, inferenceLanguage?: Host.AidaClient.AidaInferenceLanguage): void {
440
- this.#debouncedRequestAidaSuggestion(prefix, suffix, cursor, inferenceLanguage);
460
+ prefix: string, suffix: string, cursorPositionAtRequest: number,
461
+ inferenceLanguage?: Host.AidaClient.AidaInferenceLanguage): void {
462
+ this.#debouncedRequestAidaSuggestion(prefix, suffix, cursorPositionAtRequest, inferenceLanguage);
441
463
  }
442
464
 
443
465
  remove(): void {
@@ -451,8 +473,8 @@ export class AiCodeCompletion extends Common.ObjectWrapper.ObjectWrapper<EventTy
451
473
  }
452
474
  }
453
475
 
454
- export const enum Panel {
455
- CONSOLE = 'console',
476
+ export const enum ContextFlavor {
477
+ CONSOLE = 'console', // generated code can contain console specific APIs like `$0`.
456
478
  SOURCES = 'sources',
457
479
  }
458
480
 
@@ -12,6 +12,7 @@ export enum BadgeAction {
12
12
  MODERN_DOM_BADGE_CLICKED = 'modern-dom-badge-clicked',
13
13
  // TODO(ergunsh): Instrument performance insight clicks.
14
14
  PERFORMANCE_INSIGHT_CLICKED = 'performance-insight-clicked',
15
+ DEBUGGER_PAUSED = 'debugger-paused'
15
16
  }
16
17
 
17
18
  export type BadgeActionEvents = Record<BadgeAction, void>;
@@ -21,8 +22,12 @@ export interface BadgeContext {
21
22
  badgeActionEventTarget: Common.ObjectWrapper.ObjectWrapper<BadgeActionEvents>;
22
23
  }
23
24
 
25
+ export interface TriggerOptions {
26
+ immediate?: boolean;
27
+ }
28
+
24
29
  export abstract class Badge {
25
- #onTriggerBadge: (badge: Badge) => void;
30
+ #onTriggerBadge: (badge: Badge, opts?: TriggerOptions) => void;
26
31
  #badgeActionEventTarget: Common.ObjectWrapper.ObjectWrapper<BadgeActionEvents>;
27
32
  #eventListeners: Common.EventTarget.EventDescriptor[] = [];
28
33
  #triggeredBefore = false;
@@ -39,14 +44,14 @@ export abstract class Badge {
39
44
  }
40
45
 
41
46
  abstract handleAction(action: BadgeAction): void;
42
- protected trigger(): void {
47
+ protected trigger(opts?: TriggerOptions): void {
43
48
  if (this.#triggeredBefore) {
44
49
  return;
45
50
  }
46
51
 
47
52
  this.#triggeredBefore = true;
48
53
  this.deactivate();
49
- this.#onTriggerBadge(this);
54
+ this.#onTriggerBadge(this, opts);
50
55
  }
51
56
 
52
57
  activate(): void {
@@ -2,7 +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
 
5
- import {Badge, type BadgeAction} from './Badge.js';
5
+ import {Badge, BadgeAction} from './Badge.js';
6
6
 
7
7
  const CODE_WHISPERER_BADGE_IMAGE_URI = new URL('../../Images/code-whisperer-badge.svg', import.meta.url).toString();
8
8
  export class CodeWhispererBadge extends Badge {
@@ -11,9 +11,7 @@ export class CodeWhispererBadge extends Badge {
11
11
  override readonly title = 'Code Whisperer';
12
12
  override readonly imageUri = CODE_WHISPERER_BADGE_IMAGE_URI;
13
13
 
14
- override readonly interestedActions = [
15
- // TODO(ergunsh): Instrument related actions.
16
- ] as const;
14
+ override readonly interestedActions = [BadgeAction.DEBUGGER_PAUSED] as const;
17
15
 
18
16
  handleAction(_action: BadgeAction): void {
19
17
  this.trigger();
@@ -20,7 +20,7 @@ export class StarterBadge extends Badge {
20
20
  BadgeAction.DOM_ELEMENT_OR_ATTRIBUTE_EDITED,
21
21
  ] as const;
22
22
 
23
- handleAction(_action: BadgeAction): void {
24
- this.trigger();
23
+ handleAction(action: BadgeAction): void {
24
+ this.trigger({immediate: action === BadgeAction.GDP_SIGN_UP_COMPLETE});
25
25
  }
26
26
  }