chrome-devtools-mcp 0.0.2 → 0.2.0

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 (69) hide show
  1. package/README.md +6 -3
  2. package/build/node_modules/chrome-devtools-frontend/front_end/core/common/Settings.js +3 -32
  3. package/build/node_modules/chrome-devtools-frontend/front_end/core/i18n/i18n.js +35 -8
  4. package/build/node_modules/chrome-devtools-frontend/front_end/core/root/Runtime.js +4 -1
  5. package/build/node_modules/chrome-devtools-frontend/front_end/generated/InspectorBackendCommands.js +4 -4
  6. package/build/node_modules/chrome-devtools-frontend/front_end/generated/SupportedCSSProperties.js +12 -0
  7. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.js +366 -0
  8. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AICallTree.js +366 -0
  9. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AIContext.js +64 -0
  10. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AIQueries.js +105 -0
  11. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/CSSWorkspaceBinding.js +243 -0
  12. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/CompilerScriptMapping.js +407 -0
  13. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/ContentProviderBasedProject.js +128 -0
  14. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/DebuggerLanguagePlugins.js +992 -0
  15. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/DebuggerWorkspaceBinding.js +574 -0
  16. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/DefaultScriptMapping.js +112 -0
  17. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/FileUtils.js +186 -0
  18. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/LiveLocation.js +60 -0
  19. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/NetworkProject.js +107 -0
  20. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/PresentationConsoleMessageHelper.js +244 -0
  21. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/ResourceMapping.js +473 -0
  22. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/ResourceScriptMapping.js +399 -0
  23. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/ResourceUtils.js +87 -0
  24. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/SASSSourceMapping.js +181 -0
  25. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/StylesSourceMapping.js +268 -0
  26. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/TempFile.js +55 -0
  27. package/build/node_modules/chrome-devtools-frontend/front_end/models/bindings/bindings.js +20 -0
  28. package/build/node_modules/chrome-devtools-frontend/front_end/models/crux-manager/CrUXManager.js +283 -0
  29. package/build/node_modules/chrome-devtools-frontend/front_end/models/crux-manager/crux-manager.js +4 -0
  30. package/build/node_modules/chrome-devtools-frontend/front_end/models/emulation/DeviceModeModel.js +775 -0
  31. package/build/node_modules/chrome-devtools-frontend/front_end/models/emulation/EmulatedDevices.js +1706 -0
  32. package/build/node_modules/chrome-devtools-frontend/front_end/models/emulation/emulation.js +6 -0
  33. package/build/node_modules/chrome-devtools-frontend/front_end/models/formatter/FormatterWorkerPool.js +131 -0
  34. package/build/node_modules/chrome-devtools-frontend/front_end/models/formatter/ScriptFormatter.js +77 -0
  35. package/build/node_modules/chrome-devtools-frontend/front_end/models/formatter/formatter.js +6 -0
  36. package/build/node_modules/chrome-devtools-frontend/front_end/models/geometry/GeometryImpl.js +347 -0
  37. package/build/node_modules/chrome-devtools-frontend/front_end/models/geometry/geometry.js +4 -0
  38. package/build/node_modules/chrome-devtools-frontend/front_end/models/source_map_scopes/NamesResolver.js +626 -0
  39. package/build/node_modules/chrome-devtools-frontend/front_end/models/source_map_scopes/ScopeChainModel.js +59 -0
  40. package/build/node_modules/chrome-devtools-frontend/front_end/models/source_map_scopes/ScopeTreeCache.js +32 -0
  41. package/build/node_modules/chrome-devtools-frontend/front_end/models/source_map_scopes/source_map_scopes.js +7 -0
  42. package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/StackTrace.js +4 -0
  43. package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/StackTraceImpl.js +67 -0
  44. package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/StackTraceModel.js +97 -0
  45. package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/Trie.js +113 -0
  46. package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/stack_trace.js +5 -0
  47. package/build/node_modules/chrome-devtools-frontend/front_end/models/stack_trace/stack_trace_impl.js +7 -0
  48. package/build/node_modules/chrome-devtools-frontend/front_end/models/text_utils/TextUtils.js +23 -0
  49. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/Processor.js +1 -1
  50. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/helpers/Trace.js +1 -1
  51. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/DocumentLatency.js +5 -4
  52. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace_source_maps_resolver/SourceMapsResolver.js +199 -0
  53. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace_source_maps_resolver/trace_source_maps_resolver.js +4 -0
  54. package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/FileManager.js +64 -0
  55. package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/IgnoreListManager.js +511 -0
  56. package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/SearchConfig.js +113 -0
  57. package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/UISourceCode.js +563 -0
  58. package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/WorkspaceImpl.js +204 -0
  59. package/build/node_modules/chrome-devtools-frontend/front_end/models/workspace/workspace.js +9 -0
  60. package/build/src/McpContext.js +24 -9
  61. package/build/src/McpResponse.js +3 -3
  62. package/build/src/browser.js +3 -1
  63. package/build/src/index.js +1 -1
  64. package/build/src/tools/input.js +7 -7
  65. package/build/src/tools/performance.js +29 -2
  66. package/build/src/tools/screenshot.js +1 -1
  67. package/build/src/tools/script.js +40 -14
  68. package/build/src/trace-processing/parse.js +26 -22
  69. package/package.json +9 -7
@@ -0,0 +1,366 @@
1
+ // Copyright 2024 The Chromium Authors
2
+ // Use of this source code is governed by a BSD-style license that can be
3
+ // found in the LICENSE file.
4
+ import * as Root from '../../../core/root/root.js';
5
+ import * as Trace from '../../../models/trace/trace.js';
6
+ import * as SourceMapsResolver from '../../../models/trace_source_maps_resolver/trace_source_maps_resolver.js';
7
+ /** Iterates from a node down through its descendents. If the callback returns true, the loop stops. */
8
+ function depthFirstWalk(nodes, callback) {
9
+ for (const node of nodes) {
10
+ if (callback?.(node)) {
11
+ break;
12
+ }
13
+ depthFirstWalk(node.children().values(), callback); // Go deeper.
14
+ }
15
+ }
16
+ export class AICallTree {
17
+ selectedNode;
18
+ rootNode;
19
+ parsedTrace;
20
+ constructor(selectedNode, rootNode, parsedTrace) {
21
+ this.selectedNode = selectedNode;
22
+ this.rootNode = rootNode;
23
+ this.parsedTrace = parsedTrace;
24
+ }
25
+ static findEventsForThread({ thread, parsedTrace, bounds }) {
26
+ const threadEvents = parsedTrace.data.Renderer.processes.get(thread.pid)?.threads.get(thread.tid)?.entries;
27
+ if (!threadEvents) {
28
+ return null;
29
+ }
30
+ return threadEvents.filter(e => Trace.Helpers.Timing.eventIsInBounds(e, bounds));
31
+ }
32
+ static findMainThreadTasks({ thread, parsedTrace, bounds }) {
33
+ const threadEvents = parsedTrace.data.Renderer.processes.get(thread.pid)?.threads.get(thread.tid)?.entries;
34
+ if (!threadEvents) {
35
+ return null;
36
+ }
37
+ return threadEvents.filter(Trace.Types.Events.isRunTask)
38
+ .filter(e => Trace.Helpers.Timing.eventIsInBounds(e, bounds));
39
+ }
40
+ /**
41
+ * Builds a call tree representing all calls within the given timeframe for
42
+ * the provided thread.
43
+ * Events that are less than 0.05% of the range duration are removed.
44
+ */
45
+ static fromTimeOnThread({ thread, parsedTrace, bounds }) {
46
+ const overlappingEvents = this.findEventsForThread({ thread, parsedTrace, bounds });
47
+ if (!overlappingEvents) {
48
+ return null;
49
+ }
50
+ const visibleEventsFilter = new Trace.Extras.TraceFilter.VisibleEventsFilter(Trace.Styles.visibleTypes());
51
+ // By default, we remove events whose duration is less than 0.5% of the total
52
+ // range. So if the range is 10s, an event must be 0.05s+ to be included.
53
+ // This does risk eliminating useful data when we pass it to the LLM, but
54
+ // we are trying to balance context window sizes and not using it up too
55
+ // eagerly. We will experiment with this filter and likely make it smarter
56
+ // or tweak it based on range size rather than using a blanket value. Or we
57
+ // could consider limiting the depth when we serialize. Or some
58
+ // combination!
59
+ const minDuration = Trace.Types.Timing.Micro(bounds.range * 0.005);
60
+ const minDurationFilter = new MinDurationFilter(minDuration);
61
+ const compileCodeFilter = new ExcludeCompileCodeFilter();
62
+ // Build a tree bounded by the selected event's timestamps, and our other filters applied
63
+ const rootNode = new Trace.Extras.TraceTree.TopDownRootNode(overlappingEvents, {
64
+ filters: [minDurationFilter, compileCodeFilter, visibleEventsFilter],
65
+ startTime: Trace.Helpers.Timing.microToMilli(bounds.min),
66
+ endTime: Trace.Helpers.Timing.microToMilli(bounds.max),
67
+ doNotAggregate: true,
68
+ includeInstantEvents: true,
69
+ });
70
+ const instance = new AICallTree(null /* no selected node*/, rootNode, parsedTrace);
71
+ return instance;
72
+ }
73
+ /**
74
+ * Attempts to build an AICallTree from a given selected event. It also
75
+ * validates that this event is one that we support being used with the AI
76
+ * Assistance panel, which [as of January 2025] means:
77
+ * 1. It is on the main thread.
78
+ * 2. It exists in either the Renderer or Sample handler's entryToNode map.
79
+ * This filters out other events we make such as SyntheticLayoutShifts which are not valid
80
+ * If the event is not valid, or there is an unexpected error building the tree, `null` is returned.
81
+ */
82
+ static fromEvent(selectedEvent, parsedTrace) {
83
+ // Special case: performance.mark events are shown on the main thread
84
+ // technically, but because they are instant events they are shown with a
85
+ // tiny duration. Because they are instant, they also don't have any
86
+ // children or a call tree, and so if the user has selected a performance
87
+ // mark in the timings track, we do not want to attempt to build a call
88
+ // tree. Context: crbug.com/418223469
89
+ // Note that we do not have to repeat this check for performance.measure
90
+ // events because those are synthetic, and therefore the check
91
+ // further down about if this event is known to the RenderHandler
92
+ // deals with this.
93
+ if (Trace.Types.Events.isPerformanceMark(selectedEvent)) {
94
+ return null;
95
+ }
96
+ // First: check that the selected event is on the thread we have identified as the main thread.
97
+ const threads = Trace.Handlers.Threads.threadsInTrace(parsedTrace.data);
98
+ const thread = threads.find(t => t.pid === selectedEvent.pid && t.tid === selectedEvent.tid);
99
+ if (!thread) {
100
+ return null;
101
+ }
102
+ // We allow two thread types to deal with the NodeJS use case.
103
+ // MAIN_THREAD is used when a trace has been generated through Chrome
104
+ // tracing on a website (and we have a renderer)
105
+ // CPU_PROFILE is used only when we have received a CPUProfile - in this
106
+ // case all the threads are CPU_PROFILE so we allow those. If we only allow
107
+ // MAIN_THREAD then we wouldn't ever allow NodeJS users to use the AI
108
+ // integration.
109
+ if (thread.type !== "MAIN_THREAD" /* Trace.Handlers.Threads.ThreadType.MAIN_THREAD */ &&
110
+ thread.type !== "CPU_PROFILE" /* Trace.Handlers.Threads.ThreadType.CPU_PROFILE */) {
111
+ return null;
112
+ }
113
+ // Ensure that the event is known to either the Renderer or Samples
114
+ // handler. This helps exclude synthetic events we build up for other
115
+ // information such as Layout Shift clusters.
116
+ // We check Renderer + Samples to ensure we support CPU Profiles (which do
117
+ // not populate the Renderer Handler)
118
+ const data = parsedTrace.data;
119
+ if (!data.Renderer.entryToNode.has(selectedEvent) && !data.Samples.entryToNode.has(selectedEvent)) {
120
+ return null;
121
+ }
122
+ const allEventsEnabled = Root.Runtime.experiments.isEnabled('timeline-show-all-events');
123
+ const { startTime, endTime } = Trace.Helpers.Timing.eventTimingsMilliSeconds(selectedEvent);
124
+ const selectedEventBounds = Trace.Helpers.Timing.traceWindowFromMicroSeconds(Trace.Helpers.Timing.milliToMicro(startTime), Trace.Helpers.Timing.milliToMicro(endTime));
125
+ let threadEvents = data.Renderer.processes.get(selectedEvent.pid)?.threads.get(selectedEvent.tid)?.entries;
126
+ if (!threadEvents) {
127
+ // None from the renderer: try the samples handler, this might be a CPU trace.
128
+ threadEvents = data.Samples.profilesInProcess.get(selectedEvent.pid)?.get(selectedEvent.tid)?.profileCalls;
129
+ }
130
+ if (!threadEvents) {
131
+ console.warn(`AICallTree: could not find thread for selected entry: ${selectedEvent}`);
132
+ return null;
133
+ }
134
+ const overlappingEvents = threadEvents.filter(e => Trace.Helpers.Timing.eventIsInBounds(e, selectedEventBounds));
135
+ const filters = [new SelectedEventDurationFilter(selectedEvent), new ExcludeCompileCodeFilter(selectedEvent)];
136
+ // If the "Show all events" experiment is on, we don't filter out any
137
+ // events here, otherwise the generated call tree will not match what the
138
+ // user is seeing.
139
+ if (!allEventsEnabled) {
140
+ filters.push(new Trace.Extras.TraceFilter.VisibleEventsFilter(Trace.Styles.visibleTypes()));
141
+ }
142
+ // Build a tree bounded by the selected event's timestamps, and our other filters applied
143
+ const rootNode = new Trace.Extras.TraceTree.TopDownRootNode(overlappingEvents, {
144
+ filters,
145
+ startTime,
146
+ endTime,
147
+ includeInstantEvents: true,
148
+ });
149
+ // Walk the tree to find selectedNode
150
+ let selectedNode = null;
151
+ depthFirstWalk([rootNode].values(), node => {
152
+ if (node.event === selectedEvent) {
153
+ selectedNode = node;
154
+ return true;
155
+ }
156
+ return;
157
+ });
158
+ if (selectedNode === null) {
159
+ console.warn(`Selected event ${selectedEvent} not found within its own tree.`);
160
+ return null;
161
+ }
162
+ const instance = new AICallTree(selectedNode, rootNode, parsedTrace);
163
+ // instance.logDebug();
164
+ return instance;
165
+ }
166
+ /**
167
+ * Iterates through nodes level by level using a Breadth-First Search (BFS) algorithm.
168
+ * BFS is important here because the serialization process assumes that direct child nodes
169
+ * will have consecutive IDs (horizontally across each depth).
170
+ *
171
+ * Example tree with IDs:
172
+ *
173
+ * 1
174
+ * / \
175
+ * 2 3
176
+ * / / / \
177
+ * 4 5 6 7
178
+ *
179
+ * Here, node with an ID 2 has consecutive children in the 4-6 range.
180
+ *
181
+ * To optimize for space, the provided `callback` function is called to serialize
182
+ * each node as it's visited during the BFS traversal.
183
+ *
184
+ * When serializing a node, the callback receives:
185
+ * 1. The current node being visited.
186
+ * 2. The ID assigned to this current node (a simple incrementing index based on visit order).
187
+ * 3. The predicted starting ID for the children of this current node.
188
+ *
189
+ * A serialized node needs to know the ID range of its children. However,
190
+ * child node IDs are only assigned when those children are themselves visited.
191
+ * To handle this, we predict the starting ID for a node's children. This prediction
192
+ * is based on a running count of all nodes that have ever been added to the BFS queue.
193
+ * Since IDs are assigned consecutively as nodes are processed from the queue, and a
194
+ * node's children are added to the end of the queue when the parent is visited,
195
+ * their eventual IDs will follow this running count.
196
+ */
197
+ breadthFirstWalk(nodes, serializeNodeCallback) {
198
+ const queue = Array.from(nodes);
199
+ let nodeIndex = 1;
200
+ // To predict the visited children indexes
201
+ let nodesAddedToQueueCount = queue.length;
202
+ let currentNode = queue.shift();
203
+ while (currentNode) {
204
+ if (currentNode.children().size > 0) {
205
+ serializeNodeCallback(currentNode, nodeIndex, nodesAddedToQueueCount + 1);
206
+ }
207
+ else {
208
+ serializeNodeCallback(currentNode, nodeIndex);
209
+ }
210
+ queue.push(...Array.from(currentNode.children().values()));
211
+ nodesAddedToQueueCount += currentNode.children().size;
212
+ currentNode = queue.shift();
213
+ nodeIndex++;
214
+ }
215
+ }
216
+ serialize(headerLevel = 1) {
217
+ const header = '#'.repeat(headerLevel);
218
+ // Keep a map of URLs. We'll output a LUT to keep size down.
219
+ const allUrls = [];
220
+ let nodesStr = '';
221
+ this.breadthFirstWalk(this.rootNode.children().values(), (node, nodeId, childStartingNode) => {
222
+ nodesStr +=
223
+ '\n' + this.stringifyNode(node, nodeId, this.parsedTrace, this.selectedNode, allUrls, childStartingNode);
224
+ });
225
+ let output = '';
226
+ if (allUrls.length) {
227
+ // Output lookup table of URLs within this tree
228
+ output += `\n${header} All URLs:\n\n` + allUrls.map((url, index) => ` * ${index}: ${url}`).join('\n');
229
+ }
230
+ output += `\n\n${header} Call tree:\n${nodesStr}`;
231
+ return output;
232
+ }
233
+ /*
234
+ * Each node is serialized into a single line to minimize token usage in the context window.
235
+ * The format is a semicolon-separated string with the following fields:
236
+ * Format: `id;name;duration;selfTime;urlIndex;childRange;[S]
237
+ *
238
+ * 1. `id`: A unique numerical identifier for the node assigned by BFS.
239
+ * 2. `name`: The name of the event represented by the node.
240
+ * 3. `duration`: The total duration of the event in milliseconds, rounded to one decimal place.
241
+ * 4. `selfTime`: The self time of the event in milliseconds, rounded to one decimal place.
242
+ * 5. `urlIndex`: An index referencing a URL in the `allUrls` array. If no URL is present, this is an empty string.
243
+ * 6. `childRange`: A string indicating the range of IDs for the node's children. Children should always have consecutive IDs.
244
+ * If there is only one child, it's a single ID.
245
+ * 7. `[S]`: An optional marker indicating that this node is the selected node.
246
+ *
247
+ * Example:
248
+ * `1;Parse HTML;2.5;0.3;0;2-5;S`
249
+ * This represents:
250
+ * - Node ID 1
251
+ * - Name "Parse HTML"
252
+ * - Total duration of 2.5ms
253
+ * - Self time of 0.3ms
254
+ * - URL index 0 (meaning the URL is the first one in the `allUrls` array)
255
+ * - Child range of IDs 2 to 5
256
+ * - This node is the selected node (S marker)
257
+ */
258
+ stringifyNode(node, nodeId, parsedTrace, selectedNode, allUrls, childStartingNodeIndex) {
259
+ const event = node.event;
260
+ if (!event) {
261
+ throw new Error('Event required');
262
+ }
263
+ // 1. ID
264
+ const idStr = String(nodeId);
265
+ // 2. Name
266
+ const name = Trace.Name.forEntry(event, parsedTrace);
267
+ // Round milliseconds to one decimal place, return empty string if zero/undefined
268
+ const roundToTenths = (num) => {
269
+ if (!num) {
270
+ return '';
271
+ }
272
+ return String(Math.round(num * 10) / 10);
273
+ };
274
+ // 3. Duration
275
+ const durationStr = roundToTenths(node.totalTime);
276
+ // 4. Self Time
277
+ const selfTimeStr = roundToTenths(node.selfTime);
278
+ // 5. URL Index
279
+ const url = SourceMapsResolver.SourceMapsResolver.resolvedURLForEntry(parsedTrace, event);
280
+ let urlIndexStr = '';
281
+ if (url) {
282
+ const existingIndex = allUrls.indexOf(url);
283
+ if (existingIndex === -1) {
284
+ urlIndexStr = String(allUrls.push(url) - 1);
285
+ }
286
+ else {
287
+ urlIndexStr = String(existingIndex);
288
+ }
289
+ }
290
+ // 6. Child Range
291
+ const children = Array.from(node.children().values());
292
+ let childRangeStr = '';
293
+ if (childStartingNodeIndex) {
294
+ childRangeStr = (children.length === 1) ? String(childStartingNodeIndex) :
295
+ `${childStartingNodeIndex}-${childStartingNodeIndex + children.length}`;
296
+ }
297
+ // 7. Selected Marker
298
+ const selectedMarker = selectedNode?.event === node.event ? 'S' : '';
299
+ // Combine fields
300
+ let line = idStr;
301
+ line += ';' + name;
302
+ line += ';' + durationStr;
303
+ line += ';' + selfTimeStr;
304
+ line += ';' + urlIndexStr;
305
+ line += ';' + childRangeStr;
306
+ if (selectedMarker) {
307
+ line += ';' + selectedMarker;
308
+ }
309
+ return line;
310
+ }
311
+ // Only used for debugging.
312
+ logDebug() {
313
+ const str = this.serialize();
314
+ // eslint-disable-next-line no-console
315
+ console.log('🎆', str);
316
+ if (str.length > 45_000) {
317
+ // Manual testing shows 45k fits. 50k doesn't.
318
+ // Max is 32k _tokens_, but tokens to bytes is wishywashy, so... hard to know for sure.
319
+ console.warn('Output will likely not fit in the context window. Expect an AIDA error.');
320
+ }
321
+ }
322
+ }
323
+ /**
324
+ * These events are very noisy and take up room in the context window for no real benefit.
325
+ */
326
+ export class ExcludeCompileCodeFilter extends Trace.Extras.TraceFilter.TraceFilter {
327
+ #selectedEvent = null;
328
+ constructor(selectedEvent) {
329
+ super();
330
+ this.#selectedEvent = selectedEvent ?? null;
331
+ }
332
+ accept(event) {
333
+ if (this.#selectedEvent && event === this.#selectedEvent) {
334
+ // If the user selects this event, we should accept it, else the
335
+ // behaviour is confusing when the selected event is not used.
336
+ return true;
337
+ }
338
+ return event.name !== "V8.CompileCode" /* Trace.Types.Events.Name.COMPILE_CODE */;
339
+ }
340
+ }
341
+ export class SelectedEventDurationFilter extends Trace.Extras.TraceFilter.TraceFilter {
342
+ #minDuration;
343
+ #selectedEvent;
344
+ constructor(selectedEvent) {
345
+ super();
346
+ // The larger the selected event is, the less small ones matter. We'll exclude items under ½% of the selected event's size
347
+ this.#minDuration = Trace.Types.Timing.Micro((selectedEvent.dur ?? 1) * 0.005);
348
+ this.#selectedEvent = selectedEvent;
349
+ }
350
+ accept(event) {
351
+ if (event === this.#selectedEvent) {
352
+ return true;
353
+ }
354
+ return event.dur ? event.dur >= this.#minDuration : false;
355
+ }
356
+ }
357
+ export class MinDurationFilter extends Trace.Extras.TraceFilter.TraceFilter {
358
+ #minDuration;
359
+ constructor(minDuration) {
360
+ super();
361
+ this.#minDuration = minDuration;
362
+ }
363
+ accept(event) {
364
+ return event.dur ? event.dur >= this.#minDuration : false;
365
+ }
366
+ }
@@ -0,0 +1,64 @@
1
+ // Copyright 2025 The Chromium Authors
2
+ // Use of this source code is governed by a BSD-style license that can be
3
+ // found in the LICENSE file.
4
+ import * as Trace from '../../../models/trace/trace.js';
5
+ function getFirstInsightSet(insights) {
6
+ // Currently only support a single insight set. Pick the first one with a navigation.
7
+ // TODO(cjamcl): we should just give the agent the entire insight set, and give
8
+ // summary detail about all of them + the ability to query each.
9
+ return [...insights.values()].filter(insightSet => insightSet.navigation).at(0) ?? null;
10
+ }
11
+ export class AgentFocus {
12
+ static full(parsedTrace) {
13
+ if (!parsedTrace.insights) {
14
+ throw new Error('missing insights');
15
+ }
16
+ const insightSet = getFirstInsightSet(parsedTrace.insights);
17
+ return new AgentFocus({
18
+ type: 'full',
19
+ parsedTrace,
20
+ insightSet,
21
+ });
22
+ }
23
+ static fromInsight(parsedTrace, insight) {
24
+ if (!parsedTrace.insights) {
25
+ throw new Error('missing insights');
26
+ }
27
+ const insightSet = getFirstInsightSet(parsedTrace.insights);
28
+ return new AgentFocus({
29
+ type: 'insight',
30
+ parsedTrace,
31
+ insightSet,
32
+ insight,
33
+ });
34
+ }
35
+ static fromCallTree(callTree) {
36
+ const insights = callTree.parsedTrace.insights;
37
+ // Select the insight set containing the call tree.
38
+ // If for some reason that fails, fallback to the first one.
39
+ let insightSet = null;
40
+ if (insights) {
41
+ const callTreeTimeRange = Trace.Helpers.Timing.traceWindowFromEvent(callTree.rootNode.event);
42
+ insightSet = insights.values().find(set => Trace.Helpers.Timing.boundsIncludeTimeRange({
43
+ timeRange: callTreeTimeRange,
44
+ bounds: set.bounds,
45
+ })) ??
46
+ getFirstInsightSet(insights);
47
+ }
48
+ return new AgentFocus({ type: 'call-tree', parsedTrace: callTree.parsedTrace, insightSet, callTree });
49
+ }
50
+ #data;
51
+ constructor(data) {
52
+ this.#data = data;
53
+ }
54
+ get data() {
55
+ return this.#data;
56
+ }
57
+ }
58
+ export function getPerformanceAgentFocusFromModel(model) {
59
+ const parsedTrace = model.parsedTrace();
60
+ if (!parsedTrace) {
61
+ return null;
62
+ }
63
+ return AgentFocus.full(parsedTrace);
64
+ }
@@ -0,0 +1,105 @@
1
+ // Copyright 2025 The Chromium Authors
2
+ // Use of this source code is governed by a BSD-style license that can be
3
+ // found in the LICENSE file.
4
+ import * as Trace from '../../../models/trace/trace.js';
5
+ import { AICallTree } from './AICallTree.js';
6
+ export class AIQueries {
7
+ static findMainThread(navigationId, parsedTrace) {
8
+ /**
9
+ * We cannot assume that there is one main thread as there are scenarios
10
+ * where there can be multiple (see crbug.com/402658800) as an example.
11
+ * Therefore we calculate the main thread by using the thread that the
12
+ * Insight has been associated to. Most Insights relate to a navigation, so
13
+ * in this case we can use the navigation's PID/TID as we know that will
14
+ * have run on the main thread that we are interested in.
15
+ * If we do not have a navigation, we fall back to looking for the first
16
+ * thread we find that is of type MAIN_THREAD.
17
+ * Longer term we should solve this at the Trace Engine level to avoid
18
+ * look-ups like this; this is the work that is tracked in
19
+ * crbug.com/402658800.
20
+ */
21
+ let mainThreadPID = null;
22
+ let mainThreadTID = null;
23
+ if (navigationId) {
24
+ const navigation = parsedTrace.data.Meta.navigationsByNavigationId.get(navigationId);
25
+ if (navigation?.args.data?.isOutermostMainFrame) {
26
+ mainThreadPID = navigation.pid;
27
+ mainThreadTID = navigation.tid;
28
+ }
29
+ }
30
+ const threads = Trace.Handlers.Threads.threadsInTrace(parsedTrace.data);
31
+ const thread = threads.find(thread => {
32
+ if (mainThreadPID && mainThreadTID) {
33
+ return thread.pid === mainThreadPID && thread.tid === mainThreadTID;
34
+ }
35
+ return thread.type === "MAIN_THREAD" /* Trace.Handlers.Threads.ThreadType.MAIN_THREAD */;
36
+ });
37
+ return thread ?? null;
38
+ }
39
+ /**
40
+ * Returns bottom up activity for the given range.
41
+ */
42
+ static mainThreadActivityBottomUp(navigationId, bounds, parsedTrace) {
43
+ const thread = this.findMainThread(navigationId, parsedTrace);
44
+ if (!thread) {
45
+ return null;
46
+ }
47
+ const events = AICallTree.findEventsForThread({ thread, parsedTrace, bounds });
48
+ if (!events) {
49
+ return null;
50
+ }
51
+ // Use the same filtering as front_end/panels/timeline/TimelineTreeView.ts.
52
+ const visibleEvents = Trace.Helpers.Trace.VISIBLE_TRACE_EVENT_TYPES.values().toArray();
53
+ const filter = new Trace.Extras.TraceFilter.VisibleEventsFilter(visibleEvents.concat(["SyntheticNetworkRequest" /* Trace.Types.Events.Name.SYNTHETIC_NETWORK_REQUEST */]));
54
+ // The bottom up root node handles all the "in Tracebounds" checks we need for the insight.
55
+ const startTime = Trace.Helpers.Timing.microToMilli(bounds.min);
56
+ const endTime = Trace.Helpers.Timing.microToMilli(bounds.max);
57
+ return new Trace.Extras.TraceTree.BottomUpRootNode(events, {
58
+ textFilter: new Trace.Extras.TraceFilter.ExclusiveNameFilter([]),
59
+ filters: [filter],
60
+ startTime,
61
+ endTime,
62
+ });
63
+ }
64
+ /**
65
+ * Returns an AI Call Tree representing the activity on the main thread for
66
+ * the relevant time range of the given insight.
67
+ */
68
+ static mainThreadActivityTopDown(navigationId, bounds, parsedTrace) {
69
+ const thread = this.findMainThread(navigationId, parsedTrace);
70
+ if (!thread) {
71
+ return null;
72
+ }
73
+ return AICallTree.fromTimeOnThread({
74
+ thread: {
75
+ pid: thread.pid,
76
+ tid: thread.tid,
77
+ },
78
+ parsedTrace,
79
+ bounds,
80
+ });
81
+ }
82
+ /**
83
+ * Returns the top longest tasks as AI Call Trees.
84
+ */
85
+ static longestTasks(navigationId, bounds, parsedTrace, limit = 3) {
86
+ const thread = this.findMainThread(navigationId, parsedTrace);
87
+ if (!thread) {
88
+ return null;
89
+ }
90
+ const tasks = AICallTree.findMainThreadTasks({ thread, parsedTrace, bounds });
91
+ if (!tasks) {
92
+ return null;
93
+ }
94
+ const topTasks = tasks.filter(e => e.name === 'RunTask').sort((a, b) => b.dur - a.dur).slice(0, limit);
95
+ return topTasks
96
+ .map(task => {
97
+ const tree = AICallTree.fromEvent(task, parsedTrace);
98
+ if (tree) {
99
+ tree.selectedNode = null;
100
+ }
101
+ return tree;
102
+ })
103
+ .filter(tree => !!tree);
104
+ }
105
+ }