chrome-devtools-frontend 1.0.1516909 → 1.0.1519267

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 (162) hide show
  1. package/config/owner/COMMON_OWNERS +2 -2
  2. package/docs/checklist/README.md +2 -2
  3. package/docs/checklist/javascript.md +1 -1
  4. package/docs/contributing/README.md +1 -1
  5. package/docs/contributing/settings-experiments-features.md +9 -8
  6. package/docs/cookbook/devtools_on_devtools.md +2 -2
  7. package/docs/cookbook/localization.md +10 -10
  8. package/docs/devtools-protocol.md +9 -8
  9. package/docs/ecosystem/automatic_workspace_folders.md +3 -3
  10. package/docs/get_the_code.md +0 -2
  11. package/docs/styleguide/ux/components.md +166 -85
  12. package/docs/styleguide/ux/numbers.md +3 -4
  13. package/eslint.config.mjs +1 -0
  14. package/front_end/core/common/README.md +13 -12
  15. package/front_end/core/host/GdpClient.ts +16 -1
  16. package/front_end/core/host/UserMetrics.ts +4 -2
  17. package/front_end/core/root/Runtime.ts +13 -0
  18. package/front_end/core/sdk/CSSMatchedStyles.ts +5 -1
  19. package/front_end/core/sdk/EnhancedTracesParser.ts +5 -5
  20. package/front_end/core/sdk/RehydratingConnection.snapshot.txt +211 -0
  21. package/front_end/core/sdk/TargetManager.ts +4 -0
  22. package/front_end/entrypoints/main/MainImpl.ts +6 -3
  23. package/front_end/generated/InspectorBackendCommands.js +10 -7
  24. package/front_end/generated/SupportedCSSProperties.js +40 -11
  25. package/front_end/generated/protocol-mapping.d.ts +16 -1
  26. package/front_end/generated/protocol-proxy-api.d.ts +13 -1
  27. package/front_end/generated/protocol.ts +95 -0
  28. package/front_end/models/ai_assistance/agents/AiAgent.ts +57 -10
  29. package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +119 -51
  30. package/front_end/models/ai_assistance/agents/StylingAgent.ts +0 -31
  31. package/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.snapshot.txt +14 -181
  32. package/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.ts +19 -315
  33. package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.snapshot.txt +224 -50
  34. package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.ts +310 -11
  35. package/front_end/models/ai_assistance/performance/AIContext.ts +15 -2
  36. package/front_end/models/ai_code_completion/AiCodeCompletion.ts +22 -11
  37. package/front_end/models/badges/AiExplorerBadge.ts +19 -3
  38. package/front_end/models/badges/Badge.ts +10 -3
  39. package/front_end/models/badges/CodeWhispererBadge.ts +3 -4
  40. package/front_end/models/badges/DOMDetectiveBadge.ts +1 -0
  41. package/front_end/models/badges/SpeedsterBadge.ts +1 -0
  42. package/front_end/models/badges/StarterBadge.ts +3 -2
  43. package/front_end/models/badges/UserBadges.ts +21 -3
  44. package/front_end/models/badges/badges.ts +1 -0
  45. package/front_end/models/javascript_metadata/NativeFunctions.js +2 -2
  46. package/front_end/models/trace/EventsSerializer.ts +4 -3
  47. package/front_end/models/trace/README.md +28 -1
  48. package/front_end/models/trace/handlers/UserInteractionsHandler.ts +101 -73
  49. package/front_end/models/trace/handlers/UserTimingsHandler.ts +1 -1
  50. package/front_end/models/trace/helpers/Timing.ts +1 -1
  51. package/front_end/models/trace/helpers/Trace.ts +99 -43
  52. package/front_end/models/trace/types/TraceEvents.ts +9 -0
  53. package/front_end/panels/accessibility/ARIAAttributesView.ts +113 -191
  54. package/front_end/panels/accessibility/AccessibilityNodeView.ts +9 -9
  55. package/front_end/panels/accessibility/AccessibilitySubPane.ts +6 -4
  56. package/front_end/panels/accessibility/accessibilityProperties.css +2 -0
  57. package/front_end/panels/ai_assistance/AiAssistancePanel.ts +20 -3
  58. package/front_end/panels/ai_assistance/components/ChatView.ts +9 -10
  59. package/front_end/panels/ai_assistance/components/PerformanceAgentMarkdownRenderer.ts +44 -0
  60. package/front_end/panels/application/components/BounceTrackingMitigationsView.ts +2 -2
  61. package/front_end/panels/common/AiCodeCompletionDisclaimer.ts +32 -9
  62. package/front_end/panels/common/AiCodeCompletionSummaryToolbar.ts +7 -1
  63. package/front_end/panels/common/BadgeNotification.ts +21 -5
  64. package/front_end/panels/common/GdpSignUpDialog.ts +20 -12
  65. package/front_end/panels/console/ConsolePrompt.ts +1 -1
  66. package/front_end/panels/console/ConsoleView.ts +6 -2
  67. package/front_end/panels/css_overview/CSSOverviewCompletedView.ts +5 -5
  68. package/front_end/panels/elements/ElementsPanel.ts +4 -0
  69. package/front_end/panels/elements/ElementsTreeElement.ts +18 -0
  70. package/front_end/panels/elements/ElementsTreeOutline.ts +13 -0
  71. package/front_end/panels/elements/StylePropertyTreeElement.ts +21 -6
  72. package/front_end/panels/media/TickingFlameChart.ts +1 -1
  73. package/front_end/panels/profiler/HeapSnapshotView.ts +34 -19
  74. package/front_end/panels/recorder/components/RecordingView.ts +2 -2
  75. package/front_end/panels/search/SearchResultsPane.ts +167 -152
  76. package/front_end/panels/search/SearchView.ts +36 -26
  77. package/front_end/panels/search/searchResultsPane.css +9 -0
  78. package/front_end/panels/security/CookieControlsView.ts +2 -1
  79. package/front_end/panels/settings/AISettingsTab.ts +6 -3
  80. package/front_end/panels/settings/components/SyncSection.ts +39 -17
  81. package/front_end/panels/settings/emulation/components/UserAgentClientHintsForm.ts +1 -1
  82. package/front_end/panels/sources/AiCodeCompletionPlugin.ts +9 -1
  83. package/front_end/panels/sources/SourcesPanel.ts +4 -1
  84. package/front_end/panels/sources/sourcesView.css +6 -1
  85. package/front_end/panels/timeline/AppenderUtils.ts +2 -2
  86. package/front_end/panels/timeline/ExtensionTrackAppender.ts +13 -4
  87. package/front_end/panels/timeline/GPUTrackAppender.ts +2 -1
  88. package/front_end/panels/timeline/InteractionsTrackAppender.ts +5 -1
  89. package/front_end/panels/timeline/LayoutShiftsTrackAppender.ts +2 -1
  90. package/front_end/panels/timeline/ThreadAppender.ts +12 -3
  91. package/front_end/panels/timeline/TimelineFlameChartDataProvider.ts +9 -4
  92. package/front_end/panels/timeline/TimelinePanel.ts +3 -2
  93. package/front_end/panels/timeline/TimelineUIUtils.ts +5 -4
  94. package/front_end/panels/timeline/TimingsTrackAppender.ts +6 -1
  95. package/front_end/panels/timeline/components/CPUThrottlingSelector.ts +95 -82
  96. package/front_end/panels/timeline/components/LayoutShiftDetails.ts +1 -1
  97. package/front_end/panels/timeline/components/LiveMetricsView.ts +2 -2
  98. package/front_end/panels/timeline/components/NetworkRequestDetails.ts +1 -1
  99. package/front_end/panels/timeline/components/RelatedInsightChips.ts +1 -1
  100. package/front_end/panels/timeline/components/SidebarSingleInsightSet.ts +1 -1
  101. package/front_end/panels/timeline/components/cpuThrottlingSelector.css +17 -15
  102. package/front_end/panels/timeline/components/insights/BaseInsightComponent.ts +3 -0
  103. package/front_end/third_party/chromium/README.chromium +1 -1
  104. package/front_end/third_party/codemirror.next/chunk/codemirror.js +1 -1
  105. package/front_end/third_party/codemirror.next/chunk/codemirror.js.map +1 -1
  106. package/front_end/third_party/codemirror.next/codemirror.next.d.ts +6 -9
  107. package/front_end/third_party/codemirror.next/package.json +2 -1
  108. package/front_end/third_party/diff/README.chromium +1 -0
  109. package/front_end/third_party/puppeteer/README.chromium +2 -2
  110. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/core/Realm.d.ts +2 -2
  111. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/generated/injected.d.ts +1 -1
  112. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/generated/injected.js +1 -1
  113. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/generated/version.d.ts +1 -1
  114. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/generated/version.js +1 -1
  115. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/injected/injected.d.ts +1 -1
  116. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/node/ChromeLauncher.d.ts.map +1 -1
  117. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/node/ChromeLauncher.js +1 -0
  118. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/node/ChromeLauncher.js.map +1 -1
  119. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/revisions.d.ts +3 -3
  120. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/revisions.js +3 -3
  121. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/revisions.js.map +1 -1
  122. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/util/Function.d.ts.map +1 -1
  123. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/util/Function.js +16 -25
  124. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/util/Function.js.map +1 -1
  125. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/util/Mutex.d.ts +2 -2
  126. package/front_end/third_party/puppeteer/package/lib/es5-iife/puppeteer-core-browser.js +19 -28
  127. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/generated/injected.d.ts +1 -1
  128. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/generated/injected.js +1 -1
  129. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/generated/version.d.ts +1 -1
  130. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/generated/version.js +1 -1
  131. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/node/ChromeLauncher.d.ts.map +1 -1
  132. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/node/ChromeLauncher.js +1 -0
  133. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/node/ChromeLauncher.js.map +1 -1
  134. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/revisions.d.ts +3 -3
  135. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/revisions.js +3 -3
  136. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/revisions.js.map +1 -1
  137. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/util/Function.d.ts.map +1 -1
  138. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/util/Function.js +16 -25
  139. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/util/Function.js.map +1 -1
  140. package/front_end/third_party/puppeteer/package/package.json +10 -3
  141. package/front_end/third_party/puppeteer/package/src/generated/injected.ts +1 -1
  142. package/front_end/third_party/puppeteer/package/src/generated/version.ts +1 -1
  143. package/front_end/third_party/puppeteer/package/src/node/ChromeLauncher.ts +1 -0
  144. package/front_end/third_party/puppeteer/package/src/revisions.ts +3 -3
  145. package/front_end/third_party/puppeteer/package/src/util/Function.ts +22 -30
  146. package/front_end/ui/components/dialogs/Dialog.ts +1 -1
  147. package/front_end/ui/components/markdown_view/MarkdownImage.ts +4 -5
  148. package/front_end/ui/components/switch/SwitchImpl.ts +12 -1
  149. package/front_end/ui/components/text_editor/config.ts +22 -9
  150. package/front_end/ui/components/tooltips/Tooltip.ts +70 -31
  151. package/front_end/ui/legacy/README.md +33 -24
  152. package/front_end/ui/legacy/SearchableView.ts +19 -26
  153. package/front_end/ui/legacy/TextPrompt.ts +166 -1
  154. package/front_end/ui/legacy/Treeoutline.ts +19 -3
  155. package/front_end/ui/legacy/UIUtils.ts +15 -2
  156. package/front_end/ui/legacy/XElement.ts +0 -43
  157. package/front_end/ui/legacy/components/perf_ui/FlameChart.ts +20 -4
  158. package/front_end/ui/legacy/components/source_frame/XMLView.ts +12 -11
  159. package/front_end/ui/lit/i18n-template.ts +5 -2
  160. package/front_end/ui/visual_logging/KnownContextValues.ts +23 -6
  161. package/front_end/ui/visual_logging/README.md +43 -27
  162. package/package.json +1 -1
@@ -4,9 +4,10 @@
4
4
 
5
5
  import * as Common from '../../core/common/common.js';
6
6
  import * as Host from '../../core/host/host.js';
7
+ import * as Root from '../../core/root/root.js';
7
8
 
8
9
  import {AiExplorerBadge} from './AiExplorerBadge.js';
9
- import type {Badge, BadgeAction, BadgeActionEvents, BadgeContext} from './Badge.js';
10
+ import type {Badge, BadgeAction, BadgeActionEvents, BadgeContext, TriggerOptions} from './Badge.js';
10
11
  import {CodeWhispererBadge} from './CodeWhispererBadge.js';
11
12
  import {DOMDetectiveBadge} from './DOMDetectiveBadge.js';
12
13
  import {SpeedsterBadge} from './SpeedsterBadge.js';
@@ -24,6 +25,7 @@ export interface EventTypes {
24
25
 
25
26
  const SNOOZE_TIME_MS = 24 * 60 * 60 * 1000; // 24 hours
26
27
  const MAX_SNOOZE_COUNT = 3;
28
+ const DELAY_BEFORE_TRIGGER = 1500;
27
29
 
28
30
  let userBadgesInstance: UserBadges|undefined = undefined;
29
31
  export class UserBadges extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
@@ -48,6 +50,10 @@ export class UserBadges extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
48
50
  super();
49
51
 
50
52
  this.#receiveBadgesSetting = Common.Settings.Settings.instance().moduleSetting('receive-gdp-badges');
53
+ if (Host.GdpClient.getGdpProfilesEnterprisePolicy() ===
54
+ Root.Runtime.GdpProfilesEnterprisePolicyValue.ENABLED_WITHOUT_BADGES) {
55
+ this.#receiveBadgesSetting.set(false);
56
+ }
51
57
  this.#receiveBadgesSetting.addChangeListener(this.#reconcileBadges, this);
52
58
 
53
59
  this.#starterBadgeSnoozeCount = Common.Settings.Settings.instance().createSetting(
@@ -93,7 +99,8 @@ export class UserBadges extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
93
99
  this.#badgeActionEventTarget.dispatchEventToListeners(action);
94
100
  }
95
101
 
96
- async #onTriggerBadge(badge: Badge): Promise<void> {
102
+ async #onTriggerBadge(badge: Badge, opts?: TriggerOptions): Promise<void> {
103
+ const triggerTime = Date.now();
97
104
  let shouldAwardBadge = false;
98
105
  // By default, we award non-starter badges directly when they are triggered.
99
106
  if (!badge.isStarterBadge) {
@@ -116,7 +123,12 @@ export class UserBadges extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
116
123
  }
117
124
  }
118
125
 
119
- this.dispatchEventToListeners(Events.BADGE_TRIGGERED, badge);
126
+ const timeElapsedAfterTriggerCall = Date.now() - triggerTime;
127
+ // We want to add exactly 1.5 second delay between the trigger action & the notification.
128
+ const delay = opts?.immediate ? 0 : Math.max(DELAY_BEFORE_TRIGGER - timeElapsedAfterTriggerCall, 0);
129
+ setTimeout(() => {
130
+ this.dispatchEventToListeners(Events.BADGE_TRIGGERED, badge);
131
+ }, delay);
120
132
  }
121
133
 
122
134
  #deactivateAllBadges(): void {
@@ -145,6 +157,12 @@ export class UserBadges extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
145
157
  return;
146
158
  }
147
159
 
160
+ if (!Host.GdpClient.isGdpProfilesAvailable() ||
161
+ Host.GdpClient.getGdpProfilesEnterprisePolicy() !== Root.Runtime.GdpProfilesEnterprisePolicyValue.ENABLED) {
162
+ this.#deactivateAllBadges();
163
+ return;
164
+ }
165
+
148
166
  const [gdpProfile, isEligibleToCreateProfile] = await Promise.all([
149
167
  Host.GdpClient.GdpClient.instance().getProfile(),
150
168
  Host.GdpClient.GdpClient.instance().isEligibleToCreateProfile(),
@@ -2,6 +2,7 @@
2
2
  // Use of this source code is governed by a BSD-style license that can be
3
3
  // found in the LICENSE file.
4
4
 
5
+ export * from './AiExplorerBadge.js';
5
6
  export * from './SpeedsterBadge.js';
6
7
  export * from './StarterBadge.js';
7
8
  export * from './Badge.js';
@@ -7978,7 +7978,7 @@ export const NativeFunctions = [
7978
7978
  },
7979
7979
  {
7980
7980
  name: "constant",
7981
- signatures: [["tensor"],["desc","buffer"]]
7981
+ signatures: [["tensor"],["desc","buffer"],["type","value"]]
7982
7982
  },
7983
7983
  {
7984
7984
  name: "argMin",
@@ -8470,7 +8470,7 @@ export const NativeFunctions = [
8470
8470
  },
8471
8471
  {
8472
8472
  name: "submitPrintJob",
8473
- signatures: [["job_name","document","attributes"]]
8473
+ signatures: [["job_name","document_data","attributes"]]
8474
8474
  },
8475
8475
  {
8476
8476
  name: "PushEvent",
@@ -39,7 +39,7 @@ export class EventsSerializer {
39
39
  if (EventsSerializer.isLegacyTimelineFrameKey(eventValues)) {
40
40
  const event = parsedTrace.data.Frames.frames.at(eventValues.rawIndex);
41
41
  if (!event) {
42
- throw new Error(`Could not find frame with index ${eventValues.rawIndex}`);
42
+ throw new Error(`Unknown trace event. Could not find frame with index ${eventValues.rawIndex}`);
43
43
  }
44
44
  return event;
45
45
  }
@@ -48,7 +48,8 @@ export class EventsSerializer {
48
48
  const syntheticEvents = Helpers.SyntheticEvents.SyntheticEventsManager.getActiveManager().getSyntheticTraces();
49
49
  const syntheticEvent = syntheticEvents.at(eventValues.rawIndex);
50
50
  if (!syntheticEvent) {
51
- throw new Error(`Attempted to get a synthetic event from an unknown raw event index: ${eventValues.rawIndex}`);
51
+ throw new Error(`Unknown trace event. Attempted to get a synthetic event from an unknown raw event index: ${
52
+ eventValues.rawIndex}`);
52
53
  }
53
54
  return syntheticEvent;
54
55
  }
@@ -57,7 +58,7 @@ export class EventsSerializer {
57
58
  const rawEvents = Helpers.SyntheticEvents.SyntheticEventsManager.getActiveManager().getRawTraceEvents();
58
59
  return rawEvents[eventValues.rawIndex];
59
60
  }
60
- throw new Error(`Unknown trace event serializable key values: ${(eventValues as unknown[]).join('-')}`);
61
+ throw new Error(`Unknown trace event. Serializable key values: ${(eventValues as unknown[]).join('-')}`);
61
62
  }
62
63
 
63
64
  static isProfileCallKey(key: Types.File.SerializableKeyValues): key is Types.File.ProfileCallKeyValues {
@@ -31,7 +31,7 @@ This folder contains the new trace engine that was first implemented for the Per
31
31
 
32
32
 
33
33
  ┌──────────────────▼─────────────────┐
34
- │const data = model.parsedTrace()│
34
+ │const data = model.parsedTrace()
35
35
  └────────────────────────────────────┘
36
36
  ```
37
37
 
@@ -98,3 +98,30 @@ The object returned from `parsedTrace()` is an object of key-value pairs where e
98
98
  // and so on for each enabled Handler
99
99
  }
100
100
  ```
101
+
102
+ ## Pairing begin & end events
103
+
104
+ Note: this detail is not useful if you are using the Trace Engine, but it is if you are working on it.
105
+
106
+ Trace events are often emitted as `begin` & `end` events to represent the lifetime of the event. These have the `b` and `e` phase.
107
+
108
+ When we find these events, we often try to pair them into what we call a "Synthetic" event. This is a trace event that doesn't exist in the raw trace, but one that we create to make it easier to deal with. This means we can represent a `b` & `e` pair as a single event rather than pass two events around.
109
+
110
+ When we pair these events, we look for an ID. Some events have a top level `id` field, others have a nested `id2.local` field (this is for various historical reasons). `getSyntheticId` in `Trace.Helpers` takes care of this, and tries to account for potential collisions by appending a few other pieces of metadata onto the ID.
111
+
112
+ This approach worked well until July 2025 when an upstream change in Perfetto [https://chromium.googlesource.com/external/github.com/google/perfetto.git/+/aef636b27ffbf379fd722e7798030da2c5c4d699] meant that Perfetto will try to minimise the amount of unique IDs it uses. A consequence of this change is that IDs can be reused by consecutive, non-overlapping events.
113
+
114
+ For example, take the following set of events:
115
+
116
+ ```
117
+ === E1 === === E2 ===
118
+ ```
119
+
120
+ These could have the same ID, because they do not overlap.
121
+
122
+ Whereas these events will have different IDs because otherwise you cannot reliably pair them up:
123
+
124
+ ```
125
+ === E1 ===
126
+ === E2 ===
127
+ ```
@@ -2,6 +2,7 @@
2
2
  // Use of this source code is governed by a BSD-style license that can be
3
3
  // found in the LICENSE file.
4
4
 
5
+ import * as Platform from '../../../core/platform/platform.js';
5
6
  import * as Helpers from '../helpers/helpers.js';
6
7
  import * as Types from '../types/types.js';
7
8
 
@@ -56,15 +57,15 @@ let longestInteractionEvent: Types.Events.SyntheticInteractionPair|null = null;
56
57
 
57
58
  let interactionEvents: Types.Events.SyntheticInteractionPair[] = [];
58
59
  let interactionEventsWithNoNesting: Types.Events.SyntheticInteractionPair[] = [];
59
- let eventTimingEndEventsById = new Map<string, Types.Events.EventTimingEnd>();
60
60
  let eventTimingStartEventsForInteractions: Types.Events.EventTimingBegin[] = [];
61
+ let eventTimingEndEventsForInteractions: Types.Events.EventTimingEnd[] = [];
61
62
 
62
63
  export function reset(): void {
63
64
  beginCommitCompositorFrameEvents = [];
64
65
  parseMetaViewportEvents = [];
65
66
  interactionEvents = [];
66
67
  eventTimingStartEventsForInteractions = [];
67
- eventTimingEndEventsById = new Map();
68
+ eventTimingEndEventsForInteractions = [];
68
69
  interactionEventsWithNoNesting = [];
69
70
  longestInteractionEvent = null;
70
71
  }
@@ -86,7 +87,7 @@ export function handleEvent(event: Types.Events.Event): void {
86
87
 
87
88
  if (Types.Events.isEventTimingEnd(event)) {
88
89
  // Store the end event; for each start event that is an interaction, we need the matching end event to calculate the duration correctly.
89
- eventTimingEndEventsById.set(event.id, event);
90
+ eventTimingEndEventsForInteractions.push(event);
90
91
  }
91
92
 
92
93
  // From this point on we want to find events that represent interactions.
@@ -167,9 +168,18 @@ export function categoryOfInteraction(interaction: Types.Events.SyntheticInterac
167
168
  * =======B=[keyup]=====
168
169
  * ====C=[pointerdown]=
169
170
  * =D=[pointerup]=
171
+ *
172
+ * Additionally, this method will also maximise the processing duration of the
173
+ * events that we keep as non-nested. We want to make sure we give an accurate
174
+ * representation of main thread activity, so if we keep an event + hide its
175
+ * nested children, we set the top level event's processing start &
176
+ * processing end to be the earliest processing start & the latest processing
177
+ * end of its children. This ensures we report a more accurate main thread
178
+ * activity time which is important as we want developers to focus on fixing
179
+ * this.
170
180
  **/
171
- export function removeNestedInteractions(interactions: readonly Types.Events.SyntheticInteractionPair[]):
172
- readonly Types.Events.SyntheticInteractionPair[] {
181
+ export function removeNestedInteractionsAndSetProcessingTime(
182
+ interactions: readonly Types.Events.SyntheticInteractionPair[]): readonly Types.Events.SyntheticInteractionPair[] {
173
183
  /**
174
184
  * Because we nest events only that are in the same category, we store the
175
185
  * longest event for a given end time by category.
@@ -257,77 +267,95 @@ function writeSyntheticTimespans(event: Types.Events.SyntheticInteractionPair):
257
267
  export async function finalize(): Promise<void> {
258
268
  const {navigationsByFrameId} = metaHandlerData();
259
269
 
260
- // For each interaction start event, find the async end event by the ID, and then create the Synthetic Interaction event.
261
- for (const interactionStartEvent of eventTimingStartEventsForInteractions) {
262
- const endEvent = eventTimingEndEventsById.get(interactionStartEvent.id);
263
- if (!endEvent) {
264
- // If we cannot find an end event, bail and drop this event.
265
- continue;
266
- }
267
- const {type, interactionId, timeStamp, processingStart, processingEnd} = interactionStartEvent.args.data;
268
- if (!type || !interactionId || !timeStamp || !processingStart || !processingEnd) {
269
- // A valid interaction event that we care about has to have a type (e.g. pointerdown, keyup).
270
- // We also need to ensure it has an interactionId and various timings. There are edge cases where these aren't included in the trace event.
271
- continue;
272
- }
273
-
274
- // In the future we will add microsecond timestamps to the trace events…
275
- // (See https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/timing/window_performance.cc;l=900-901;drc=b503c262e425eae59ced4a80d59d176ed07152c7 )
276
- // …but until then we can use the millisecond precision values that are in
277
- // the trace event. To adjust them to be relative to the event.ts and the
278
- // trace timestamps, for both processingStart and processingEnd we subtract
279
- // the event timestamp (NOT event.ts, but the timeStamp millisecond value
280
- // emitted in args.data), and then add that value to the event.ts. This
281
- // will give us a processingStart and processingEnd time in microseconds
282
- // that is relative to event.ts, and can be used when drawing boxes.
283
- // There is some inaccuracy here as we are converting milliseconds to microseconds, but it is good enough until the backend emits more accurate numbers.
284
- const processingStartRelativeToTraceTime = Types.Timing.Micro(
285
- Helpers.Timing.milliToMicro(processingStart) - Helpers.Timing.milliToMicro(timeStamp) +
286
- interactionStartEvent.ts,
287
- );
288
-
289
- const processingEndRelativeToTraceTime = Types.Timing.Micro(
290
- (Helpers.Timing.milliToMicro(processingEnd) - Helpers.Timing.milliToMicro(timeStamp)) +
291
- interactionStartEvent.ts);
292
-
293
- // Ultimate frameId fallback only needed for TSC, see comments in the type.
294
- const frameId = interactionStartEvent.args.frame ?? interactionStartEvent.args.data.frame ?? '';
295
- const navigation = Helpers.Trace.getNavigationForTraceEvent(interactionStartEvent, frameId, navigationsByFrameId);
296
- const navigationId = navigation?.args.data?.navigationId;
297
- const interactionEvent =
298
- Helpers.SyntheticEvents.SyntheticEventsManager.registerSyntheticEvent<Types.Events.SyntheticInteractionPair>({
299
- // Use the start event to define the common fields.
300
- rawSourceEvent: interactionStartEvent,
301
- cat: interactionStartEvent.cat,
302
- name: interactionStartEvent.name,
303
- pid: interactionStartEvent.pid,
304
- tid: interactionStartEvent.tid,
305
- ph: interactionStartEvent.ph,
306
- processingStart: processingStartRelativeToTraceTime,
307
- processingEnd: processingEndRelativeToTraceTime,
308
- // These will be set in writeSyntheticTimespans()
309
- inputDelay: Types.Timing.Micro(-1),
310
- mainThreadHandling: Types.Timing.Micro(-1),
311
- presentationDelay: Types.Timing.Micro(-1),
312
- args: {
313
- data: {
314
- beginEvent: interactionStartEvent,
315
- endEvent,
316
- frame: frameId,
317
- navigationId,
270
+ const beginAndEndEvents = Platform.ArrayUtilities.mergeOrdered(
271
+ eventTimingStartEventsForInteractions, eventTimingEndEventsForInteractions,
272
+ Helpers.Trace.eventTimeComparator) as Types.Events.EventTimingBeginOrEnd[];
273
+
274
+ // Pair up the begin & end events and create synthetic user timing events.
275
+ const beginEventById = new Map<string, Types.Events.EventTimingBegin[]>();
276
+ for (const event of beginAndEndEvents) {
277
+ if (Types.Events.isEventTimingStart(event)) {
278
+ const forId = beginEventById.get(event.id) ?? [];
279
+ forId.push(event);
280
+ beginEventById.set(event.id, forId);
281
+ } else if (Types.Events.isEventTimingEnd(event)) {
282
+ const beginEvents = beginEventById.get(event.id) ?? [];
283
+ const beginEvent = beginEvents.pop();
284
+ if (!beginEvent) {
285
+ continue;
286
+ }
287
+ const {type, interactionId, timeStamp, processingStart, processingEnd} = beginEvent.args.data;
288
+ if (!type || !interactionId || !timeStamp || !processingStart || !processingEnd) {
289
+ // A valid interaction event that we care about has to have a type (e.g. pointerdown, keyup).
290
+ // We also need to ensure it has an interactionId and various timings. There are edge cases where these aren't included in the trace event.
291
+ continue;
292
+ }
293
+ // In the future we will add microsecond timestamps to the trace events…
294
+ // (See https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/timing/window_performance.cc;l=900-901;drc=b503c262e425eae59ced4a80d59d176ed07152c7 )
295
+ // …but until then we can use the millisecond precision values that are in
296
+ // the trace event. To adjust them to be relative to the event.ts and the
297
+ // trace timestamps, for both processingStart and processingEnd we subtract
298
+ // the event timestamp (NOT event.ts, but the timeStamp millisecond value
299
+ // emitted in args.data), and then add that value to the event.ts. This
300
+ // will give us a processingStart and processingEnd time in microseconds
301
+ // that is relative to event.ts, and can be used when drawing boxes.
302
+ // There is some inaccuracy here as we are converting milliseconds to
303
+ // microseconds, but it is good enough until the backend emits more
304
+ // accurate numbers.
305
+ const processingStartRelativeToTraceTime = Types.Timing.Micro(
306
+ Helpers.Timing.milliToMicro(processingStart) - Helpers.Timing.milliToMicro(timeStamp) + beginEvent.ts,
307
+ );
308
+
309
+ const processingEndRelativeToTraceTime = Types.Timing.Micro(
310
+ (Helpers.Timing.milliToMicro(processingEnd) - Helpers.Timing.milliToMicro(timeStamp)) + beginEvent.ts);
311
+
312
+ // Ultimate frameId fallback only needed for TSC, see comments in the type.
313
+ const frameId = beginEvent.args.frame ?? beginEvent.args.data.frame ?? '';
314
+ const navigation = Helpers.Trace.getNavigationForTraceEvent(beginEvent, frameId, navigationsByFrameId);
315
+ const navigationId = navigation?.args.data?.navigationId;
316
+ const interactionEvent =
317
+ Helpers.SyntheticEvents.SyntheticEventsManager.registerSyntheticEvent<Types.Events.SyntheticInteractionPair>({
318
+ // Use the start event to define the common fields.
319
+ rawSourceEvent: beginEvent,
320
+ cat: beginEvent.cat,
321
+ name: beginEvent.name,
322
+ pid: beginEvent.pid,
323
+ tid: beginEvent.tid,
324
+ ph: beginEvent.ph,
325
+ processingStart: processingStartRelativeToTraceTime,
326
+ processingEnd: processingEndRelativeToTraceTime,
327
+ // These will be set in writeSyntheticTimespans()
328
+ inputDelay: Types.Timing.Micro(-1),
329
+ mainThreadHandling: Types.Timing.Micro(-1),
330
+ presentationDelay: Types.Timing.Micro(-1),
331
+ args: {
332
+ data: {
333
+ beginEvent,
334
+ endEvent: event,
335
+ frame: frameId,
336
+ navigationId,
337
+ },
318
338
  },
319
- },
320
- ts: interactionStartEvent.ts,
321
- dur: Types.Timing.Micro(endEvent.ts - interactionStartEvent.ts),
322
- type: interactionStartEvent.args.data.type,
323
- interactionId: interactionStartEvent.args.data.interactionId,
324
- });
325
- writeSyntheticTimespans(interactionEvent);
326
-
327
- interactionEvents.push(interactionEvent);
339
+ ts: beginEvent.ts,
340
+ dur: Types.Timing.Micro(event.ts - beginEvent.ts),
341
+ type: beginEvent.args.data.type,
342
+ interactionId: beginEvent.args.data.interactionId,
343
+ });
344
+ writeSyntheticTimespans(interactionEvent);
345
+ interactionEvents.push(interactionEvent);
346
+ }
328
347
  }
329
348
 
330
- interactionEventsWithNoNesting.push(...removeNestedInteractions(interactionEvents));
349
+ // Once we gather up all the interactions, we want to remove nested
350
+ // interactions. Interactions can be nested because one user action (e.g. a
351
+ // click) will cause a pointerdown, pointerup and click. But we don't want to
352
+ // fill the interactions track with lots of noise. To fix this, we go through
353
+ // all the events and remove any nested ones so on the timeline we focus the
354
+ // user on the most important events, which we define as the longest one. But
355
+ // this algorithm assumes the events are in ASC order, so we first sort the
356
+ // set of interactions.
357
+ Helpers.Trace.sortTraceEventsInPlace(interactionEvents);
358
+ interactionEventsWithNoNesting.push(...removeNestedInteractionsAndSetProcessingTime(interactionEvents));
331
359
 
332
360
  // Pick the longest interactions from the set that were not nested, as we
333
361
  // know those are the set of the largest interactions.
@@ -223,9 +223,9 @@ export async function finalize(): Promise<void> {
223
223
 
224
224
  export function data(): UserTimingsData {
225
225
  return {
226
+ consoleTimings: syntheticEvents.filter(e => e.cat === 'blink.console') as Types.Events.SyntheticConsoleTimingPair[],
226
227
  performanceMeasures: syntheticEvents.filter(e => e.cat === 'blink.user_timing') as
227
228
  Types.Events.SyntheticUserTimingPair[],
228
- consoleTimings: syntheticEvents.filter(e => e.cat === 'blink.console') as Types.Events.SyntheticConsoleTimingPair[],
229
229
  performanceMarks: performanceMarkEvents,
230
230
  timestampEvents,
231
231
  measureTraceByTraceId,
@@ -221,7 +221,7 @@ export function boundsIncludeTimeRange(data: BoundsIncludeTimeRange): boolean {
221
221
  /** Checks to see if the event is within or overlaps the bounds */
222
222
  export function eventIsInBounds(event: Types.Events.Event, bounds: Types.Timing.TraceWindowMicro): boolean {
223
223
  const startTime = event.ts;
224
- return startTime <= bounds.max && bounds.min <= (startTime + (event.dur ?? 0));
224
+ return startTime <= bounds.max && bounds.min < (startTime + (event.dur ?? 0));
225
225
  }
226
226
 
227
227
  export function timestampIsInBounds(bounds: Types.Timing.TraceWindowMicro, timestamp: Types.Timing.Micro): boolean {
@@ -12,6 +12,7 @@ import {SyntheticEventsManager} from './SyntheticEvents.js';
12
12
  import {eventTimingsMicroSeconds} from './Timing.js';
13
13
 
14
14
  interface MatchingPairableAsyncEvents {
15
+ syntheticId: string;
15
16
  begin: Types.Events.PairableAsyncBegin|null;
16
17
  end: Types.Events.PairableAsyncEnd|null;
17
18
  instant?: Types.Events.PairableAsyncInstant[];
@@ -290,62 +291,113 @@ export function makeProfileCall(
290
291
  }
291
292
 
292
293
  /**
293
- * Matches beginning events with PairableAsyncEnd and PairableAsyncInstant (ASYNC_NESTABLE_INSTANT)
294
- * if provided, though currently only coming from Animations. Traces may contain multiple instant events so we need to
295
- * account for that.
294
+ * Matches beginning events with PairableAsyncEnd and PairableAsyncInstant
295
+ * if provided. Traces may contain multiple instant events so we need to
296
+ * account for that. Additionally we have seen cases where we might only have a
297
+ * begin event & instant event(s), with no end event. So we account for that
298
+ * situation also.
296
299
  *
297
- * @returns Map of the animation's ID to it's matching events.
300
+ * You might also like to read the models/trace/README.md which has some
301
+ * documentation on trace IDs. This is important as Perfetto will reuse trace
302
+ * IDs when emitting events (if they do not overlap). This means it's not as
303
+ * simple as grouping events by IDs. Instead, we group begin & instant events
304
+ * by ID as we find them. When we find end events, we then pop any matching
305
+ * begin/instant events off the stack and group those. That way, if we meet the
306
+ * same ID later on it doesn't cause us collisions.
307
+ *
308
+ * @returns An array of all the matched event groups, along with their ID. Note
309
+ * that two event groups can have the same ID if they were non-overlapping
310
+ * events. You cannot rely on ID being unique across a trace. The returned set
311
+ * of groups are NOT SORTED in any order.
298
312
  */
299
- export function matchEvents(unpairedEvents: Types.Events.PairableAsync[]): Map<string, MatchingPairableAsyncEvents> {
313
+ function matchEvents(unpairedEvents: Types.Events.PairableAsync[]): MatchingPairableAsyncEvents[] {
314
+ sortTraceEventsInPlace(unpairedEvents);
300
315
  // map to store begin and end of the event
301
- const matchedPairs = new Map<string, MatchingPairableAsyncEvents>();
316
+ const matches: MatchingPairableAsyncEvents[] = [];
302
317
 
303
- // looking for start and end
318
+ const beginEventsById = new Map<string, Types.Events.PairableAsyncBegin[]>();
319
+ const instantEventsById = new Map<string, Types.Events.PairableAsyncInstant[]>();
304
320
  for (const event of unpairedEvents) {
305
- const syntheticId = getSyntheticId(event);
306
- if (syntheticId === undefined) {
321
+ const id = getSyntheticId(event);
322
+ if (id === undefined) {
307
323
  continue;
308
324
  }
309
- // Create a synthetic id to prevent collisions across categories.
310
- // Console timings can be dispatched with the same id, so use the
311
- // event name as well to generate unique ids.
312
- const otherEventsWithID = Platform.MapUtilities.getWithDefault(matchedPairs, syntheticId, () => {
313
- return {begin: null, end: null, instant: []};
314
- });
315
-
316
- const isStartEvent = event.ph === Types.Events.Phase.ASYNC_NESTABLE_START;
317
- const isEndEvent = event.ph === Types.Events.Phase.ASYNC_NESTABLE_END;
318
- const isInstantEvent = event.ph === Types.Events.Phase.ASYNC_NESTABLE_INSTANT;
319
-
320
- if (isStartEvent) {
321
- otherEventsWithID.begin = event as Types.Events.PairableAsyncBegin;
322
- } else if (isEndEvent) {
323
- otherEventsWithID.end = event as Types.Events.PairableAsyncEnd;
324
- } else if (isInstantEvent) {
325
- if (!otherEventsWithID.instant) {
326
- otherEventsWithID.instant = [];
325
+ if (Types.Events.isPairableAsyncBegin(event)) {
326
+ const existingEvents = beginEventsById.get(id) ?? [];
327
+ existingEvents.push(event);
328
+ beginEventsById.set(id, existingEvents);
329
+ } else if (Types.Events.isPairableAsyncInstant(event)) {
330
+ const existingEvents = instantEventsById.get(id) ?? [];
331
+ existingEvents.push(event);
332
+ instantEventsById.set(id, existingEvents);
333
+ } else if (Types.Events.isPairableAsyncEnd(event)) {
334
+ // Find matching begin event by ID
335
+ const beginEventsWithMatchingId = beginEventsById.get(id) ?? [];
336
+ const beginEvent = beginEventsWithMatchingId.pop();
337
+ if (!beginEvent) {
338
+ continue;
327
339
  }
328
- otherEventsWithID.instant.push(event as Types.Events.PairableAsyncInstant);
340
+ const instantEventsWithMatchingId = instantEventsById.get(id) ?? [];
341
+ // Find all instant events after the begin event ts.
342
+ const instantEventsForThisGroup: Types.Events.PairableAsyncInstant[] = [];
343
+ while (instantEventsWithMatchingId.length > 0) {
344
+ if (instantEventsWithMatchingId[0].ts >= beginEvent.ts) {
345
+ const event = instantEventsWithMatchingId.pop();
346
+ if (event) {
347
+ instantEventsForThisGroup.push(event);
348
+ }
349
+ } else {
350
+ break;
351
+ }
352
+ }
353
+ const matchingGroup: MatchingPairableAsyncEvents = {
354
+ begin: beginEvent,
355
+ end: event,
356
+ instant: instantEventsForThisGroup,
357
+ syntheticId: id,
358
+ };
359
+ matches.push(matchingGroup);
329
360
  }
330
361
  }
331
- return matchedPairs;
362
+
363
+ // At this point we know we have paired up all the Begin & End & Instant
364
+ // events. But it is possible to see only begin & instant events with the
365
+ // same ID, and no end event. So now we do a second pass through our begin
366
+ // events to find any that did not have an end event. If we find some
367
+ // instant events for the begin event, we create a new group.
368
+ // Also, because there were no end events, we know that the IDs will be
369
+ // unique now; e.g. each key in the map should have no more than one item in
370
+ // it.
371
+ for (const [id, beginEvents] of beginEventsById) {
372
+ const beginEvent = beginEvents.pop();
373
+ if (!beginEvent) {
374
+ continue;
375
+ }
376
+ const matchingInstantEvents = instantEventsById.get(id);
377
+ if (matchingInstantEvents?.length) {
378
+ matches.push({
379
+ syntheticId: id,
380
+ begin: beginEvent,
381
+ end: null,
382
+ instant: matchingInstantEvents,
383
+ });
384
+ }
385
+ }
386
+
387
+ return matches;
332
388
  }
333
389
 
334
- function getSyntheticId(event: Types.Events.PairableAsync): string|undefined {
390
+ export function getSyntheticId(event: Types.Events.PairableAsync): string|undefined {
335
391
  const id = extractId(event);
336
392
  return id && `${event.cat}:${id}:${event.name}`;
337
393
  }
338
394
 
339
- export function createSortedSyntheticEvents<T extends Types.Events.PairableAsync>(
340
- matchedPairs: Map<string, {
341
- begin: Types.Events.PairableAsyncBegin | null,
342
- end: Types.Events.PairableAsyncEnd | null,
343
- instant?: Types.Events.PairableAsyncInstant[],
344
- }>,
345
- syntheticEventCallback?: (syntheticEvent: Types.Events.SyntheticEventPair<T>) => void,
395
+ function createSortedSyntheticEvents<T extends Types.Events.PairableAsync>(
396
+ matchedPairs: MatchingPairableAsyncEvents[],
346
397
  ): Array<Types.Events.SyntheticEventPair<T>> {
347
398
  const syntheticEvents: Array<Types.Events.SyntheticEventPair<T>> = [];
348
- for (const [id, eventsTriplet] of matchedPairs.entries()) {
399
+ for (const eventsTriplet of matchedPairs) {
400
+ const id = eventsTriplet.syntheticId;
349
401
  const beginEvent = eventsTriplet.begin;
350
402
  const endEvent = eventsTriplet.end;
351
403
  const instantEvents = eventsTriplet.instant;
@@ -399,17 +451,21 @@ export function createSortedSyntheticEvents<T extends Types.Events.PairableAsync
399
451
  // crbug.com/1472375
400
452
  continue;
401
453
  }
402
- syntheticEventCallback?.(event);
403
454
  syntheticEvents.push(event);
404
455
  }
405
- return syntheticEvents.sort((a, b) => a.ts - b.ts);
456
+ sortTraceEventsInPlace(syntheticEvents);
457
+ return syntheticEvents;
406
458
  }
407
459
 
408
- export function createMatchedSortedSyntheticEvents<T extends Types.Events.PairableAsync>(
409
- unpairedAsyncEvents: T[], syntheticEventCallback?: (syntheticEvent: Types.Events.SyntheticEventPair<T>) => void):
460
+ /**
461
+ * Groups up sets of async events into synthetic events.
462
+ * @param unpairedAsyncEvents the raw array of begin, end and async instant
463
+ * events. These MUST be sorted in timestamp ASC order.
464
+ */
465
+ export function createMatchedSortedSyntheticEvents<T extends Types.Events.PairableAsync>(unpairedAsyncEvents: T[]):
410
466
  Array<Types.Events.SyntheticEventPair<T>> {
411
467
  const matchedPairs = matchEvents(unpairedAsyncEvents);
412
- const syntheticEvents = createSortedSyntheticEvents<T>(matchedPairs, syntheticEventCallback);
468
+ const syntheticEvents = createSortedSyntheticEvents<T>(matchedPairs);
413
469
  return syntheticEvents;
414
470
  }
415
471
 
@@ -1358,6 +1358,15 @@ export interface PairableAsyncInstant extends PairableAsync {
1358
1358
  export interface PairableAsyncEnd extends PairableAsync {
1359
1359
  ph: Phase.ASYNC_NESTABLE_END;
1360
1360
  }
1361
+ export function isPairableAsyncBegin(e: Event): e is PairableAsyncBegin {
1362
+ return e.ph === Phase.ASYNC_NESTABLE_START;
1363
+ }
1364
+ export function isPairableAsyncEnd(e: Event): e is PairableAsyncEnd {
1365
+ return e.ph === Phase.ASYNC_NESTABLE_END;
1366
+ }
1367
+ export function isPairableAsyncInstant(e: Event): e is PairableAsyncInstant {
1368
+ return e.ph === Phase.ASYNC_NESTABLE_INSTANT;
1369
+ }
1361
1370
 
1362
1371
  export interface AnimationFrame extends PairableAsync {
1363
1372
  name: Name.ANIMATION_FRAME;