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 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 CrUXManager from '../../crux-manager/crux-manager.js';
5
+ import * as Trace from '../../trace/trace.js';
6
+ import { AIQueries } from '../performance/AIQueries.js';
7
+ import { PerformanceInsightFormatter, TraceEventFormatter } from './PerformanceInsightFormatter.js';
8
+ import { bytes, micros, millis } from './UnitFormatters.js';
9
+ export class PerformanceTraceFormatter {
10
+ #parsedTrace;
11
+ #insightSet;
12
+ #eventsSerializer;
13
+ constructor(focus, eventsSerializer) {
14
+ this.#parsedTrace = focus.data.parsedTrace;
15
+ this.#insightSet = focus.data.insightSet;
16
+ this.#eventsSerializer = eventsSerializer;
17
+ }
18
+ serializeEvent(event) {
19
+ const key = this.#eventsSerializer.keyForEvent(event);
20
+ return `(eventKey: ${key}, ts: ${event.ts})`;
21
+ }
22
+ serializeBounds(bounds) {
23
+ return `{min: ${bounds.min}, max: ${bounds.max}}`;
24
+ }
25
+ /**
26
+ * Fetching the Crux summary can error outside of DevTools, hence the
27
+ * try-catch around it here.
28
+ */
29
+ #getCruxTraceSummary(insightSet) {
30
+ if (insightSet === null) {
31
+ return [];
32
+ }
33
+ try {
34
+ const cruxScope = CrUXManager.CrUXManager.instance().getSelectedScope();
35
+ const parts = [];
36
+ const fieldMetrics = Trace.Insights.Common.getFieldMetricsForInsightSet(insightSet, this.#parsedTrace.metadata, cruxScope);
37
+ const fieldLcp = fieldMetrics?.lcp;
38
+ const fieldInp = fieldMetrics?.inp;
39
+ const fieldCls = fieldMetrics?.cls;
40
+ if (fieldLcp || fieldInp || fieldCls) {
41
+ parts.push('Metrics (field / real users):');
42
+ const serializeFieldMetricTimingResult = (fieldMetric) => {
43
+ return `${Math.round(fieldMetric.value / 1000)} ms (scope: ${fieldMetric.pageScope})`;
44
+ };
45
+ const serializeFieldMetricNumberResult = (fieldMetric) => {
46
+ return `${fieldMetric.value.toFixed(2)} (scope: ${fieldMetric.pageScope})`;
47
+ };
48
+ if (fieldLcp) {
49
+ parts.push(` - LCP: ${serializeFieldMetricTimingResult(fieldLcp)}`);
50
+ const fieldLcpBreakdown = fieldMetrics?.lcpBreakdown;
51
+ if (fieldLcpBreakdown &&
52
+ (fieldLcpBreakdown.ttfb || fieldLcpBreakdown.loadDelay || fieldLcpBreakdown.loadDuration ||
53
+ fieldLcpBreakdown.renderDelay)) {
54
+ parts.push(' - LCP breakdown:');
55
+ if (fieldLcpBreakdown.ttfb) {
56
+ parts.push(` - TTFB: ${serializeFieldMetricTimingResult(fieldLcpBreakdown.ttfb)}`);
57
+ }
58
+ if (fieldLcpBreakdown.loadDelay) {
59
+ parts.push(` - Load delay: ${serializeFieldMetricTimingResult(fieldLcpBreakdown.loadDelay)}`);
60
+ }
61
+ if (fieldLcpBreakdown.loadDuration) {
62
+ parts.push(` - Load duration: ${serializeFieldMetricTimingResult(fieldLcpBreakdown.loadDuration)}`);
63
+ }
64
+ if (fieldLcpBreakdown.renderDelay) {
65
+ parts.push(` - Render delay: ${serializeFieldMetricTimingResult(fieldLcpBreakdown.renderDelay)}`);
66
+ }
67
+ }
68
+ }
69
+ if (fieldInp) {
70
+ parts.push(` - INP: ${serializeFieldMetricTimingResult(fieldInp)}`);
71
+ }
72
+ if (fieldCls) {
73
+ parts.push(` - CLS: ${serializeFieldMetricNumberResult(fieldCls)}`);
74
+ }
75
+ parts.push(' - The above data is from CrUX–Chrome User Experience Report. It\'s how the page performs for real users.');
76
+ parts.push(' - The values shown above are the p75 measure of all real Chrome users');
77
+ parts.push(' - The scope indicates if the data came from the entire origin, or a specific url');
78
+ parts.push(' - Lab metrics describe how this specific page load performed, while field metrics are an aggregation ' +
79
+ 'of results from real-world users. Best practice is to prioritize metrics that are bad in field data. ' +
80
+ 'Lab metrics may be better or worse than fields metrics depending on the developer\'s machine, network, or the ' +
81
+ 'actions performed while tracing.');
82
+ }
83
+ return parts;
84
+ }
85
+ catch {
86
+ return [];
87
+ }
88
+ }
89
+ formatTraceSummary() {
90
+ const parsedTrace = this.#parsedTrace;
91
+ const insightSet = this.#insightSet;
92
+ const traceMetadata = this.#parsedTrace.metadata;
93
+ const data = parsedTrace.data;
94
+ const parts = [];
95
+ const lcp = insightSet ? Trace.Insights.Common.getLCP(insightSet) : null;
96
+ const cls = insightSet ? Trace.Insights.Common.getCLS(insightSet) : null;
97
+ const inp = insightSet ? Trace.Insights.Common.getINP(insightSet) : null;
98
+ parts.push(`URL: ${data.Meta.mainFrameURL}`);
99
+ parts.push(`Bounds: ${this.serializeBounds(data.Meta.traceBounds)}`);
100
+ parts.push('CPU throttling: ' + (traceMetadata.cpuThrottling ? `${traceMetadata.cpuThrottling}x` : 'none'));
101
+ parts.push(`Network throttling: ${traceMetadata.networkThrottling ?? 'none'}`);
102
+ if (lcp || cls || inp) {
103
+ parts.push('Metrics (lab / observed):');
104
+ if (lcp) {
105
+ parts.push(` - LCP: ${Math.round(lcp.value / 1000)} ms, event: ${this.serializeEvent(lcp.event)}`);
106
+ const subparts = insightSet?.model.LCPBreakdown.subparts;
107
+ if (subparts) {
108
+ const serializeSubpart = (subpart) => {
109
+ return `${micros(subpart.range)}, bounds: ${this.serializeBounds(subpart)}`;
110
+ };
111
+ parts.push(' - LCP breakdown:');
112
+ parts.push(` - TTFB: ${serializeSubpart(subparts.ttfb)}`);
113
+ if (subparts.loadDelay !== undefined) {
114
+ parts.push(` - Load delay: ${serializeSubpart(subparts.loadDelay)}`);
115
+ }
116
+ if (subparts.loadDuration !== undefined) {
117
+ parts.push(` - Load duration: ${serializeSubpart(subparts.loadDuration)}`);
118
+ }
119
+ parts.push(` - Render delay: ${serializeSubpart(subparts.renderDelay)}`);
120
+ }
121
+ }
122
+ if (inp) {
123
+ parts.push(` - INP: ${Math.round(inp.value / 1000)} ms, event: ${this.serializeEvent(inp.event)}`);
124
+ }
125
+ if (cls) {
126
+ const eventText = cls.worstClusterEvent ? `, event: ${this.serializeEvent(cls.worstClusterEvent)}` : '';
127
+ parts.push(` - CLS: ${cls.value.toFixed(2)}${eventText}`);
128
+ }
129
+ }
130
+ else {
131
+ parts.push('Metrics (lab / observed): n/a');
132
+ }
133
+ const cruxParts = insightSet && this.#getCruxTraceSummary(insightSet);
134
+ if (cruxParts?.length) {
135
+ parts.push(...cruxParts);
136
+ }
137
+ else {
138
+ parts.push('Metrics (field / real users): n/a – no data for this page in CrUX');
139
+ }
140
+ if (insightSet) {
141
+ parts.push('Available insights:');
142
+ for (const [insightName, model] of Object.entries(insightSet.model)) {
143
+ if (model.state === 'pass') {
144
+ continue;
145
+ }
146
+ const formatter = new PerformanceInsightFormatter(parsedTrace, model);
147
+ if (!formatter.insightIsSupported()) {
148
+ continue;
149
+ }
150
+ const insightBounds = Trace.Insights.Common.insightBounds(model, insightSet.bounds);
151
+ const insightParts = [
152
+ `insight name: ${insightName}`,
153
+ `description: ${model.description}`,
154
+ `relevant trace bounds: ${this.serializeBounds(insightBounds)}`,
155
+ ];
156
+ const metricSavingsText = formatter.estimatedSavings();
157
+ if (metricSavingsText) {
158
+ insightParts.push(`estimated metric savings: ${metricSavingsText}`);
159
+ }
160
+ if (model.wastedBytes) {
161
+ insightParts.push(`estimated wasted bytes: ${bytes(model.wastedBytes)}`);
162
+ }
163
+ for (const suggestion of formatter.getSuggestions()) {
164
+ insightParts.push(`example question: ${suggestion.title}`);
165
+ }
166
+ const insightPartsText = insightParts.join('\n ');
167
+ parts.push(` - ${insightPartsText}`);
168
+ }
169
+ }
170
+ else {
171
+ parts.push('Available insights: none');
172
+ }
173
+ return parts.join('\n');
174
+ }
175
+ formatCriticalRequests() {
176
+ const parsedTrace = this.#parsedTrace;
177
+ const insightSet = this.#insightSet;
178
+ const criticalRequests = [];
179
+ const walkRequest = (node) => {
180
+ criticalRequests.push(node.request);
181
+ node.children.forEach(walkRequest);
182
+ };
183
+ insightSet?.model.NetworkDependencyTree.rootNodes.forEach(walkRequest);
184
+ if (!criticalRequests.length) {
185
+ return '';
186
+ }
187
+ return 'Critical network requests:\n' +
188
+ TraceEventFormatter.networkRequests(criticalRequests, parsedTrace, { verbose: false });
189
+ }
190
+ #serializeBottomUpRootNode(rootNode, limit) {
191
+ // Sorted by selfTime.
192
+ // No nodes less than 1 ms.
193
+ // Limit.
194
+ const topNodes = [...rootNode.children().values()]
195
+ .filter(n => n.totalTime >= 1)
196
+ .sort((a, b) => b.selfTime - a.selfTime)
197
+ .slice(0, limit);
198
+ function nodeToText(node) {
199
+ const event = node.event;
200
+ let frame;
201
+ if (Trace.Types.Events.isProfileCall(event)) {
202
+ frame = event.callFrame;
203
+ }
204
+ else {
205
+ frame = Trace.Helpers.Trace.getStackTraceTopCallFrameInEventPayload(event);
206
+ }
207
+ let source = Trace.Name.forEntry(event);
208
+ if (frame?.url) {
209
+ source += ` (url: ${frame.url}`;
210
+ if (frame.lineNumber !== -1) {
211
+ source += `, line: ${frame.lineNumber}`;
212
+ }
213
+ if (frame.columnNumber !== -1) {
214
+ source += `, column: ${frame.columnNumber}`;
215
+ }
216
+ source += ')';
217
+ }
218
+ return `- self: ${millis(node.selfTime)}, total: ${millis(node.totalTime)}, source: ${source}`;
219
+ }
220
+ const listText = topNodes.map(node => nodeToText.call(this, node)).join('\n');
221
+ const format = `This is the bottom-up summary for the entire trace. Only the top ${limit} activities (sorted by self time) are shown. An activity is all the aggregated time spent on the same type of work. For example, it can be all the time spent in a specific JavaScript function, or all the time spent in a specific browser rendering stage (like layout, v8 compile, parsing html). "Self time" represents the aggregated time spent directly in an activity, across all occurrences. "Total time" represents the aggregated time spent in an activity or any of its children.`;
222
+ return `${format}\n\n${listText}`;
223
+ }
224
+ formatMainThreadBottomUpSummary() {
225
+ const parsedTrace = this.#parsedTrace;
226
+ const insightSet = this.#insightSet;
227
+ const bounds = parsedTrace.data.Meta.traceBounds;
228
+ const rootNode = AIQueries.mainThreadActivityBottomUp(insightSet?.navigation?.args.data?.navigationId, bounds, parsedTrace);
229
+ if (!rootNode) {
230
+ return '';
231
+ }
232
+ return this.#serializeBottomUpRootNode(rootNode, 10);
233
+ }
234
+ #formatThirdPartyEntitySummaries(summaries) {
235
+ const topMainThreadTimeEntries = summaries.toSorted((a, b) => b.mainThreadTime - a.mainThreadTime).slice(0, 5);
236
+ if (!topMainThreadTimeEntries.length) {
237
+ return '';
238
+ }
239
+ const listText = topMainThreadTimeEntries
240
+ .map(s => {
241
+ const transferSize = `${bytes(s.transferSize)}`;
242
+ return `- name: ${s.entity.name}, main thread time: ${millis(s.mainThreadTime)}, network transfer size: ${transferSize}`;
243
+ })
244
+ .join('\n');
245
+ return listText;
246
+ }
247
+ formatThirdPartySummary() {
248
+ const insightSet = this.#insightSet;
249
+ if (!insightSet) {
250
+ return '';
251
+ }
252
+ const thirdParties = insightSet.model.ThirdParties;
253
+ let summaries = thirdParties.entitySummaries ?? [];
254
+ if (thirdParties.firstPartyEntity) {
255
+ summaries = summaries.filter(s => s.entity !== thirdParties?.firstPartyEntity || null);
256
+ }
257
+ const listText = this.#formatThirdPartyEntitySummaries(summaries);
258
+ if (!listText) {
259
+ return '';
260
+ }
261
+ return `Third party summary:\n${listText}`;
262
+ }
263
+ formatLongestTasks() {
264
+ const parsedTrace = this.#parsedTrace;
265
+ const insightSet = this.#insightSet;
266
+ const bounds = parsedTrace.data.Meta.traceBounds;
267
+ const longestTaskTrees = AIQueries.longestTasks(insightSet?.navigation?.args.data?.navigationId, bounds, parsedTrace, 3);
268
+ if (!longestTaskTrees || longestTaskTrees.length === 0) {
269
+ return 'Longest tasks: none';
270
+ }
271
+ const listText = longestTaskTrees
272
+ .map(tree => {
273
+ const time = millis(tree.rootNode.totalTime);
274
+ return `- total time: ${time}, event: ${this.serializeEvent(tree.rootNode.event)}`;
275
+ })
276
+ .join('\n');
277
+ return `Longest ${longestTaskTrees.length} tasks:\n${listText}`;
278
+ }
279
+ #serializeRelatedInsightsForEvents(events) {
280
+ if (!events.length) {
281
+ return '';
282
+ }
283
+ const insightNameToRelatedEvents = new Map();
284
+ if (this.#insightSet) {
285
+ for (const model of Object.values(this.#insightSet.model)) {
286
+ if (!model.relatedEvents) {
287
+ continue;
288
+ }
289
+ const modeRelatedEvents = Array.isArray(model.relatedEvents) ? model.relatedEvents : [...model.relatedEvents.keys()];
290
+ if (!modeRelatedEvents.length) {
291
+ continue;
292
+ }
293
+ const relatedEvents = modeRelatedEvents.filter(e => events.includes(e));
294
+ if (relatedEvents.length) {
295
+ insightNameToRelatedEvents.set(model.insightKey, relatedEvents);
296
+ }
297
+ }
298
+ }
299
+ if (!insightNameToRelatedEvents.size) {
300
+ return '';
301
+ }
302
+ const results = [];
303
+ for (const [insightKey, events] of insightNameToRelatedEvents) {
304
+ // Limit to 5, because some insights (namely ThirdParties) can have a huge
305
+ // number of related events. Mostly, insights probably don't have more than
306
+ // 5.
307
+ const eventsString = events.slice(0, 5).map(e => Trace.Name.forEntry(e) + ' ' + this.serializeEvent(e)).join(', ');
308
+ results.push(`- ${insightKey}: ${eventsString}`);
309
+ }
310
+ return results.join('\n');
311
+ }
312
+ formatMainThreadTrackSummary(bounds) {
313
+ const results = [];
314
+ const topDownTree = AIQueries.mainThreadActivityTopDown(this.#insightSet?.navigation?.args.data?.navigationId, bounds, this.#parsedTrace);
315
+ if (topDownTree) {
316
+ results.push('# Top-down main thread summary');
317
+ results.push(this.formatCallTree(topDownTree, 2 /* headerLevel */));
318
+ }
319
+ const bottomUpRootNode = AIQueries.mainThreadActivityBottomUp(this.#insightSet?.navigation?.args.data?.navigationId, bounds, this.#parsedTrace);
320
+ if (bottomUpRootNode) {
321
+ results.push('# Bottom-up main thread summary');
322
+ results.push(this.#serializeBottomUpRootNode(bottomUpRootNode, 20));
323
+ }
324
+ const thirdPartySummaries = Trace.Extras.ThirdParties.summarizeByThirdParty(this.#parsedTrace.data, bounds);
325
+ if (thirdPartySummaries.length) {
326
+ results.push('# Third parties');
327
+ results.push(this.#formatThirdPartyEntitySummaries(thirdPartySummaries));
328
+ }
329
+ const relatedInsightsText = this.#serializeRelatedInsightsForEvents([...topDownTree?.rootNode.events ?? [], ...bottomUpRootNode?.events ?? []]);
330
+ if (relatedInsightsText) {
331
+ results.push('# Related insights');
332
+ results.push('Here are all the insights that contain some related event from the main thread in the given range.');
333
+ results.push(relatedInsightsText);
334
+ }
335
+ if (!results.length) {
336
+ return 'No main thread activity found';
337
+ }
338
+ return results.join('\n\n');
339
+ }
340
+ formatNetworkTrackSummary(bounds) {
341
+ const results = [];
342
+ const requests = this.#parsedTrace.data.NetworkRequests.byTime.filter(request => Trace.Helpers.Timing.eventIsInBounds(request, bounds));
343
+ const requestsText = TraceEventFormatter.networkRequests(requests, this.#parsedTrace, { verbose: false });
344
+ results.push('# Network requests summary');
345
+ results.push(requestsText || 'No requests in the given bounds');
346
+ const relatedInsightsText = this.#serializeRelatedInsightsForEvents(requests);
347
+ if (relatedInsightsText) {
348
+ results.push('# Related insights');
349
+ results.push('Here are all the insights that contain some related request from the given range.');
350
+ results.push(relatedInsightsText);
351
+ }
352
+ return results.join('\n\n');
353
+ }
354
+ formatCallTree(tree, headerLevel = 1) {
355
+ const results = [tree.serialize(headerLevel), ''];
356
+ // TODO(b/425270067): add eventKey to tree.serialize, but need to wait for other
357
+ // performance agent to be consolidated.
358
+ results.push('#'.repeat(headerLevel) + ' Node id to eventKey\n');
359
+ results.push('These node ids correspond to the call tree nodes listed in the above section.\n');
360
+ tree.breadthFirstWalk(tree.rootNode.children().values(), (node, nodeId) => {
361
+ results.push(`${nodeId}: ${this.#eventsSerializer.keyForEvent(node.event)}`);
362
+ });
363
+ results.push('\nIMPORTANT: Never show eventKey to the user.');
364
+ return results.join('\n');
365
+ }
366
+ }