chrome-devtools-mcp 0.2.7 → 0.4.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.
- package/README.md +30 -4
- package/build/node_modules/chrome-devtools-frontend/front_end/core/host/GdpClient.js +30 -5
- package/build/node_modules/chrome-devtools-frontend/front_end/core/host/UserMetrics.js +3 -2
- package/build/node_modules/chrome-devtools-frontend/front_end/core/i18n/i18n.js +24 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSMatchedStyles.js +5 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/EnhancedTracesParser.js +4 -5
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/NetworkManager.js +1 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/NetworkRequest.js +8 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/TargetManager.js +4 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/generated/InspectorBackendCommands.js +10 -7
- package/build/node_modules/chrome-devtools-frontend/front_end/generated/SupportedCSSProperties.js +40 -11
- package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.js +86 -283
- package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.js +267 -20
- package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AICallTree.js +12 -6
- package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AIContext.js +64 -7
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/EventsSerializer.js +3 -3
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/UserInteractionsHandler.js +91 -63
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/UserTimingsHandler.js +1 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/helpers/Timing.js +1 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/helpers/Trace.js +98 -36
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/types/TraceEvents.js +9 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/third_party/legacy-javascript/legacy-javascript.js +2 -38
- package/build/node_modules/chrome-devtools-frontend/front_end/third_party/legacy-javascript/lib/legacy-javascript.js +1 -5
- package/build/node_modules/chrome-devtools-frontend/front_end/third_party/third-party-web/lib/nostats-subset.js +1 -3
- package/build/node_modules/chrome-devtools-frontend/front_end/third_party/third-party-web/third-party-web.js +2 -8
- package/build/src/McpContext.js +8 -2
- package/build/src/McpResponse.js +47 -8
- package/build/src/browser.js +2 -2
- package/build/src/cli.js +81 -0
- package/build/src/formatters/consoleFormatter.js +3 -1
- package/build/src/index.js +3 -3
- package/build/src/main.js +12 -85
- package/build/src/tools/ToolDefinition.js +1 -0
- package/build/src/tools/emulation.js +2 -2
- package/build/src/tools/input.js +8 -8
- package/build/src/tools/network.js +46 -4
- package/build/src/tools/pages.js +12 -2
- package/build/src/tools/performance.js +4 -4
- package/build/src/tools/screenshot.js +1 -1
- package/build/src/tools/script.js +4 -7
- package/build/src/tools/snapshot.js +2 -2
- package/build/src/trace-processing/parse.js +14 -7
- package/build/src/utils/pagination.js +49 -0
- package/package.json +9 -6
|
@@ -4,16 +4,19 @@
|
|
|
4
4
|
import * as CrUXManager from '../../crux-manager/crux-manager.js';
|
|
5
5
|
import * as Trace from '../../trace/trace.js';
|
|
6
6
|
import { AIQueries } from '../performance/AIQueries.js';
|
|
7
|
-
import {
|
|
7
|
+
import { NetworkRequestFormatter } from './NetworkRequestFormatter.js';
|
|
8
|
+
import { PerformanceInsightFormatter } from './PerformanceInsightFormatter.js';
|
|
8
9
|
import { bytes, micros, millis } from './UnitFormatters.js';
|
|
9
10
|
export class PerformanceTraceFormatter {
|
|
11
|
+
#focus;
|
|
10
12
|
#parsedTrace;
|
|
11
13
|
#insightSet;
|
|
12
14
|
#eventsSerializer;
|
|
13
|
-
constructor(focus
|
|
14
|
-
this.#
|
|
15
|
-
this.#
|
|
16
|
-
this.#
|
|
15
|
+
constructor(focus) {
|
|
16
|
+
this.#focus = focus;
|
|
17
|
+
this.#parsedTrace = focus.parsedTrace;
|
|
18
|
+
this.#insightSet = focus.insightSet;
|
|
19
|
+
this.#eventsSerializer = focus.eventsSerializer;
|
|
17
20
|
}
|
|
18
21
|
serializeEvent(event) {
|
|
19
22
|
const key = this.#eventsSerializer.keyForEvent(event);
|
|
@@ -102,7 +105,9 @@ export class PerformanceTraceFormatter {
|
|
|
102
105
|
if (lcp || cls || inp) {
|
|
103
106
|
parts.push('Metrics (lab / observed):');
|
|
104
107
|
if (lcp) {
|
|
105
|
-
|
|
108
|
+
const nodeId = insightSet?.model.LCPBreakdown.lcpEvent?.args.data?.nodeId;
|
|
109
|
+
const nodeIdText = nodeId !== undefined ? `, nodeId: ${nodeId}` : '';
|
|
110
|
+
parts.push(` - LCP: ${Math.round(lcp.value / 1000)} ms, event: ${this.serializeEvent(lcp.event)}${nodeIdText}`);
|
|
106
111
|
const subparts = insightSet?.model.LCPBreakdown.subparts;
|
|
107
112
|
if (subparts) {
|
|
108
113
|
const serializeSubpart = (subpart) => {
|
|
@@ -143,7 +148,7 @@ export class PerformanceTraceFormatter {
|
|
|
143
148
|
if (model.state === 'pass') {
|
|
144
149
|
continue;
|
|
145
150
|
}
|
|
146
|
-
const formatter = new PerformanceInsightFormatter(
|
|
151
|
+
const formatter = new PerformanceInsightFormatter(this.#focus, model);
|
|
147
152
|
if (!formatter.insightIsSupported()) {
|
|
148
153
|
continue;
|
|
149
154
|
}
|
|
@@ -173,7 +178,6 @@ export class PerformanceTraceFormatter {
|
|
|
173
178
|
return parts.join('\n');
|
|
174
179
|
}
|
|
175
180
|
formatCriticalRequests() {
|
|
176
|
-
const parsedTrace = this.#parsedTrace;
|
|
177
181
|
const insightSet = this.#insightSet;
|
|
178
182
|
const criticalRequests = [];
|
|
179
183
|
const walkRequest = (node) => {
|
|
@@ -184,8 +188,7 @@ export class PerformanceTraceFormatter {
|
|
|
184
188
|
if (!criticalRequests.length) {
|
|
185
189
|
return '';
|
|
186
190
|
}
|
|
187
|
-
return 'Critical network requests:\n' +
|
|
188
|
-
TraceEventFormatter.networkRequests(criticalRequests, parsedTrace, { verbose: false });
|
|
191
|
+
return 'Critical network requests:\n' + this.formatNetworkRequests(criticalRequests, { verbose: false });
|
|
189
192
|
}
|
|
190
193
|
#serializeBottomUpRootNode(rootNode, limit) {
|
|
191
194
|
// Sorted by selfTime.
|
|
@@ -340,7 +343,7 @@ export class PerformanceTraceFormatter {
|
|
|
340
343
|
formatNetworkTrackSummary(bounds) {
|
|
341
344
|
const results = [];
|
|
342
345
|
const requests = this.#parsedTrace.data.NetworkRequests.byTime.filter(request => Trace.Helpers.Timing.eventIsInBounds(request, bounds));
|
|
343
|
-
const requestsText =
|
|
346
|
+
const requestsText = this.formatNetworkRequests(requests, { verbose: false });
|
|
344
347
|
results.push('# Network requests summary');
|
|
345
348
|
results.push(requestsText || 'No requests in the given bounds');
|
|
346
349
|
const relatedInsightsText = this.#serializeRelatedInsightsForEvents(requests);
|
|
@@ -352,15 +355,259 @@ export class PerformanceTraceFormatter {
|
|
|
352
355
|
return results.join('\n\n');
|
|
353
356
|
}
|
|
354
357
|
formatCallTree(tree, headerLevel = 1) {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
358
|
+
return `${tree.serialize(headerLevel)}\n\nIMPORTANT: Never show eventKey to the user.`;
|
|
359
|
+
}
|
|
360
|
+
formatNetworkRequests(requests, options) {
|
|
361
|
+
if (requests.length === 0) {
|
|
362
|
+
return '';
|
|
363
|
+
}
|
|
364
|
+
let verbose;
|
|
365
|
+
if (options?.verbose !== undefined) {
|
|
366
|
+
verbose = options.verbose;
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
verbose = requests.length === 1;
|
|
370
|
+
}
|
|
371
|
+
// Use verbose format for a single network request. With the compressed format, a format description
|
|
372
|
+
// needs to be provided, which is not worth sending if only one network request is being stringified.
|
|
373
|
+
if (verbose) {
|
|
374
|
+
return requests.map(request => this.#networkRequestVerbosely(request, options)).join('\n');
|
|
375
|
+
}
|
|
376
|
+
return this.#networkRequestsArrayCompressed(requests);
|
|
377
|
+
}
|
|
378
|
+
#getOrAssignUrlIndex(urlIdToIndex, url) {
|
|
379
|
+
let index = urlIdToIndex.get(url);
|
|
380
|
+
if (index !== undefined) {
|
|
381
|
+
return index;
|
|
382
|
+
}
|
|
383
|
+
index = urlIdToIndex.size;
|
|
384
|
+
urlIdToIndex.set(url, index);
|
|
385
|
+
return index;
|
|
386
|
+
}
|
|
387
|
+
#getInitiatorChain(parsedTrace, request) {
|
|
388
|
+
const initiators = [];
|
|
389
|
+
let cur = request;
|
|
390
|
+
while (cur) {
|
|
391
|
+
const initiator = parsedTrace.data.NetworkRequests.eventToInitiator.get(cur);
|
|
392
|
+
if (initiator) {
|
|
393
|
+
// Should never happen, but if it did that would be an infinite loop.
|
|
394
|
+
if (initiators.includes(initiator)) {
|
|
395
|
+
return [];
|
|
396
|
+
}
|
|
397
|
+
initiators.unshift(initiator);
|
|
398
|
+
}
|
|
399
|
+
cur = initiator;
|
|
400
|
+
}
|
|
401
|
+
return initiators;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* This is the data passed to a network request when the Performance Insights
|
|
405
|
+
* agent is asking for information. It is a slimmed down version of the
|
|
406
|
+
* request's data to avoid using up too much of the context window.
|
|
407
|
+
* IMPORTANT: these set of fields have been reviewed by Chrome Privacy &
|
|
408
|
+
* Security; be careful about adding new data here. If you are in doubt please
|
|
409
|
+
* talk to jacktfranklin@.
|
|
410
|
+
*/
|
|
411
|
+
#networkRequestVerbosely(request, options) {
|
|
412
|
+
const { url, statusCode, initialPriority, priority, fromServiceWorker, mimeType, responseHeaders, syntheticData, protocol } = request.args.data;
|
|
413
|
+
const parsedTrace = this.#parsedTrace;
|
|
414
|
+
const titlePrefix = `## ${options?.customTitle ?? 'Network request'}`;
|
|
415
|
+
// Note: unlike other agents, we do have the ability to include
|
|
416
|
+
// cross-origins, hence why we do not sanitize the URLs here.
|
|
417
|
+
const navigationForEvent = Trace.Helpers.Trace.getNavigationForTraceEvent(request, request.args.data.frame, parsedTrace.data.Meta.navigationsByFrameId);
|
|
418
|
+
const baseTime = navigationForEvent?.ts ?? parsedTrace.data.Meta.traceBounds.min;
|
|
419
|
+
// Gets all the timings for this request, relative to the base time.
|
|
420
|
+
// Note that this is the start time, not total time. E.g. "queuedAt: X"
|
|
421
|
+
// means that the request was queued at Xms, not that it queued for Xms.
|
|
422
|
+
const startTimesForLifecycle = {
|
|
423
|
+
queuedAt: request.ts - baseTime,
|
|
424
|
+
requestSentAt: syntheticData.sendStartTime - baseTime,
|
|
425
|
+
downloadCompletedAt: syntheticData.finishTime - baseTime,
|
|
426
|
+
processingCompletedAt: request.ts + request.dur - baseTime,
|
|
427
|
+
};
|
|
428
|
+
const mainThreadProcessingDuration = startTimesForLifecycle.processingCompletedAt - startTimesForLifecycle.downloadCompletedAt;
|
|
429
|
+
const downloadTime = syntheticData.finishTime - syntheticData.downloadStart;
|
|
430
|
+
const renderBlocking = Trace.Helpers.Network.isSyntheticNetworkRequestEventRenderBlocking(request);
|
|
431
|
+
const initiator = parsedTrace.data.NetworkRequests.eventToInitiator.get(request);
|
|
432
|
+
const priorityLines = [];
|
|
433
|
+
if (initialPriority === priority) {
|
|
434
|
+
priorityLines.push(`Priority: ${priority}`);
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
priorityLines.push(`Initial priority: ${initialPriority}`);
|
|
438
|
+
priorityLines.push(`Final priority: ${priority}`);
|
|
439
|
+
}
|
|
440
|
+
const redirects = request.args.data.redirects.map((redirect, index) => {
|
|
441
|
+
const startTime = redirect.ts - baseTime;
|
|
442
|
+
return `#### Redirect ${index + 1}: ${redirect.url}
|
|
443
|
+
- Start time: ${micros(startTime)}
|
|
444
|
+
- Duration: ${micros(redirect.dur)}`;
|
|
362
445
|
});
|
|
363
|
-
|
|
364
|
-
|
|
446
|
+
const initiators = this.#getInitiatorChain(parsedTrace, request);
|
|
447
|
+
const initiatorUrls = initiators.map(initiator => initiator.args.data.url);
|
|
448
|
+
const eventKey = this.#eventsSerializer.keyForEvent(request);
|
|
449
|
+
const eventKeyLine = eventKey ? `eventKey: ${eventKey}\n` : '';
|
|
450
|
+
return `${titlePrefix}: ${url}
|
|
451
|
+
${eventKeyLine}Timings:
|
|
452
|
+
- Queued at: ${micros(startTimesForLifecycle.queuedAt)}
|
|
453
|
+
- Request sent at: ${micros(startTimesForLifecycle.requestSentAt)}
|
|
454
|
+
- Download complete at: ${micros(startTimesForLifecycle.downloadCompletedAt)}
|
|
455
|
+
- Main thread processing completed at: ${micros(startTimesForLifecycle.processingCompletedAt)}
|
|
456
|
+
Durations:
|
|
457
|
+
- Download time: ${micros(downloadTime)}
|
|
458
|
+
- Main thread processing time: ${micros(mainThreadProcessingDuration)}
|
|
459
|
+
- Total duration: ${micros(request.dur)}${initiator ? `\nInitiator: ${initiator.args.data.url}` : ''}
|
|
460
|
+
Redirects:${redirects.length ? '\n' + redirects.join('\n') : ' no redirects'}
|
|
461
|
+
Status code: ${statusCode}
|
|
462
|
+
MIME Type: ${mimeType}
|
|
463
|
+
Protocol: ${protocol}
|
|
464
|
+
${priorityLines.join('\n')}
|
|
465
|
+
Render blocking: ${renderBlocking ? 'Yes' : 'No'}
|
|
466
|
+
From a service worker: ${fromServiceWorker ? 'Yes' : 'No'}
|
|
467
|
+
Initiators (root request to the request that directly loaded this one): ${initiatorUrls.join(', ') || 'none'}
|
|
468
|
+
${NetworkRequestFormatter.formatHeaders('Response headers', responseHeaders ?? [], true)}`;
|
|
469
|
+
}
|
|
470
|
+
// A compact network requests format designed to save tokens when sending multiple network requests to the model.
|
|
471
|
+
// It creates a map that maps request URLs to IDs and references the IDs in the compressed format.
|
|
472
|
+
//
|
|
473
|
+
// Important: Do not use this method for stringifying a single network request. With this format, a format description
|
|
474
|
+
// needs to be provided, which is not worth sending if only one network request is being stringified.
|
|
475
|
+
// For a single request, use `formatRequestVerbosely`, which formats with all fields specified and does not require a
|
|
476
|
+
// format description.
|
|
477
|
+
#networkRequestsArrayCompressed(requests) {
|
|
478
|
+
const networkDataString = `
|
|
479
|
+
Network requests data:
|
|
480
|
+
|
|
481
|
+
`;
|
|
482
|
+
const urlIdToIndex = new Map();
|
|
483
|
+
const allRequestsText = requests
|
|
484
|
+
.map(request => {
|
|
485
|
+
const urlIndex = this.#getOrAssignUrlIndex(urlIdToIndex, request.args.data.url);
|
|
486
|
+
return this.#networkRequestCompressedFormat(urlIndex, request, urlIdToIndex);
|
|
487
|
+
})
|
|
488
|
+
.join('\n');
|
|
489
|
+
const urlsMapString = 'allUrls = ' +
|
|
490
|
+
`[${Array.from(urlIdToIndex.entries())
|
|
491
|
+
.map(([url, index]) => {
|
|
492
|
+
return `${index}: ${url}`;
|
|
493
|
+
})
|
|
494
|
+
.join(', ')}]`;
|
|
495
|
+
return networkDataString + '\n\n' + urlsMapString + '\n\n' + allRequestsText;
|
|
496
|
+
}
|
|
497
|
+
static callFrameDataFormatDescription = `Each call frame is presented in the following format:
|
|
498
|
+
|
|
499
|
+
'id;eventKey;name;duration;selfTime;urlIndex;childRange;[S]'
|
|
500
|
+
|
|
501
|
+
Key definitions:
|
|
502
|
+
|
|
503
|
+
* id: A unique numerical identifier for the call frame. Never mention this id in the output to the user.
|
|
504
|
+
* eventKey: String that uniquely identifies this event in the flame chart.
|
|
505
|
+
* name: A concise string describing the call frame (e.g., 'Evaluate Script', 'render', 'fetchData').
|
|
506
|
+
* duration: The total execution time of the call frame, including its children.
|
|
507
|
+
* selfTime: The time spent directly within the call frame, excluding its children's execution.
|
|
508
|
+
* urlIndex: Index referencing the "All URLs" list. Empty if no specific script URL is associated.
|
|
509
|
+
* childRange: Specifies the direct children of this node using their IDs. If empty ('' or 'S' at the end), the node has no children. If a single number (e.g., '4'), the node has one child with that ID. If in the format 'firstId-lastId' (e.g., '4-5'), it indicates a consecutive range of child IDs from 'firstId' to 'lastId', inclusive.
|
|
510
|
+
* S: _Optional_. The letter 'S' terminates the line if that call frame was selected by the user.
|
|
511
|
+
|
|
512
|
+
Example Call Tree:
|
|
513
|
+
|
|
514
|
+
1;r-123;main;500;100;;
|
|
515
|
+
2;r-124;update;200;50;;3
|
|
516
|
+
3;p-49575-15428179-2834-374;animate;150;20;0;4-5;S
|
|
517
|
+
4;p-49575-15428179-3505-1162;calculatePosition;80;80;;
|
|
518
|
+
5;p-49575-15428179-5391-2767;applyStyles;50;50;;
|
|
519
|
+
`;
|
|
520
|
+
/**
|
|
521
|
+
* Network requests format description that is sent to the model as a fact.
|
|
522
|
+
*/
|
|
523
|
+
static networkDataFormatDescription = `Network requests are formatted like this:
|
|
524
|
+
\`urlIndex;eventKey;queuedTime;requestSentTime;downloadCompleteTime;processingCompleteTime;totalDuration;downloadDuration;mainThreadProcessingDuration;statusCode;mimeType;priority;initialPriority;finalPriority;renderBlocking;protocol;fromServiceWorker;initiators;redirects:[[redirectUrlIndex|startTime|duration]];responseHeaders:[header1Value|header2Value|...]\`
|
|
525
|
+
|
|
526
|
+
- \`urlIndex\`: Numerical index for the request's URL, referencing the "All URLs" list.
|
|
527
|
+
- \`eventKey\`: String that uniquely identifies this request's trace event.
|
|
528
|
+
Timings (all in milliseconds, relative to navigation start):
|
|
529
|
+
- \`queuedTime\`: When the request was queued.
|
|
530
|
+
- \`requestSentTime\`: When the request was sent.
|
|
531
|
+
- \`downloadCompleteTime\`: When the download completed.
|
|
532
|
+
- \`processingCompleteTime\`: When main thread processing finished.
|
|
533
|
+
Durations (all in milliseconds):
|
|
534
|
+
- \`totalDuration\`: Total time from the request being queued until its main thread processing completed.
|
|
535
|
+
- \`downloadDuration\`: Time spent actively downloading the resource.
|
|
536
|
+
- \`mainThreadProcessingDuration\`: Time spent on the main thread after the download completed.
|
|
537
|
+
- \`statusCode\`: The HTTP status code of the response (e.g., 200, 404).
|
|
538
|
+
- \`mimeType\`: The MIME type of the resource (e.g., "text/html", "application/javascript").
|
|
539
|
+
- \`priority\`: The final network request priority (e.g., "VeryHigh", "Low").
|
|
540
|
+
- \`initialPriority\`: The initial network request priority.
|
|
541
|
+
- \`finalPriority\`: The final network request priority (redundant if \`priority\` is always final, but kept for clarity if \`initialPriority\` and \`priority\` differ).
|
|
542
|
+
- \`renderBlocking\`: 't' if the request was render-blocking, 'f' otherwise.
|
|
543
|
+
- \`protocol\`: The network protocol used (e.g., "h2", "http/1.1").
|
|
544
|
+
- \`fromServiceWorker\`: 't' if the request was served from a service worker, 'f' otherwise.
|
|
545
|
+
- \`initiators\`: A list (separated by ,) of URL indices for the initiator chain of this request. Listed in order starting from the root request to the request that directly loaded this one. This represents the network dependencies necessary to load this request. If there is no initiator, this is empty.
|
|
546
|
+
- \`redirects\`: A comma-separated list of redirects, enclosed in square brackets. Each redirect is formatted as
|
|
547
|
+
\`[redirectUrlIndex|startTime|duration]\`, where: \`redirectUrlIndex\`: Numerical index for the redirect's URL. \`startTime\`: The start time of the redirect in milliseconds, relative to navigation start. \`duration\`: The duration of the redirect in milliseconds.
|
|
548
|
+
- \`responseHeaders\`: A list (separated by '|') of values for specific, pre-defined response headers, enclosed in square brackets.
|
|
549
|
+
The order of headers corresponds to an internal fixed list. If a header is not present, its value will be empty.
|
|
550
|
+
`;
|
|
551
|
+
/**
|
|
552
|
+
* This is the network request data passed to the Performance agent.
|
|
553
|
+
*
|
|
554
|
+
* The `urlIdToIndex` Map is used to map URLs to numerical indices in order to not need to pass whole url every time it's mentioned.
|
|
555
|
+
* The map content is passed in the response together will all the requests data.
|
|
556
|
+
*
|
|
557
|
+
* See `networkDataFormatDescription` above for specifics.
|
|
558
|
+
*/
|
|
559
|
+
#networkRequestCompressedFormat(urlIndex, request, urlIdToIndex) {
|
|
560
|
+
const { statusCode, initialPriority, priority, fromServiceWorker, mimeType, responseHeaders, syntheticData, protocol, } = request.args.data;
|
|
561
|
+
const parsedTrace = this.#parsedTrace;
|
|
562
|
+
const navigationForEvent = Trace.Helpers.Trace.getNavigationForTraceEvent(request, request.args.data.frame, parsedTrace.data.Meta.navigationsByFrameId);
|
|
563
|
+
const baseTime = navigationForEvent?.ts ?? parsedTrace.data.Meta.traceBounds.min;
|
|
564
|
+
const queuedTime = micros(request.ts - baseTime);
|
|
565
|
+
const requestSentTime = micros(syntheticData.sendStartTime - baseTime);
|
|
566
|
+
const downloadCompleteTime = micros(syntheticData.finishTime - baseTime);
|
|
567
|
+
const processingCompleteTime = micros(request.ts + request.dur - baseTime);
|
|
568
|
+
const totalDuration = micros(request.dur);
|
|
569
|
+
const downloadDuration = micros(syntheticData.finishTime - syntheticData.downloadStart);
|
|
570
|
+
const mainThreadProcessingDuration = micros(request.ts + request.dur - syntheticData.finishTime);
|
|
571
|
+
const renderBlocking = Trace.Helpers.Network.isSyntheticNetworkRequestEventRenderBlocking(request) ? 't' : 'f';
|
|
572
|
+
const finalPriority = priority;
|
|
573
|
+
const headerValues = responseHeaders
|
|
574
|
+
?.map(header => {
|
|
575
|
+
const value = NetworkRequestFormatter.allowHeader(header.name) ? header.value : '<redacted>';
|
|
576
|
+
return `${header.name}: ${value}`;
|
|
577
|
+
})
|
|
578
|
+
.join('|');
|
|
579
|
+
const redirects = request.args.data.redirects
|
|
580
|
+
.map(redirect => {
|
|
581
|
+
const urlIndex = this.#getOrAssignUrlIndex(urlIdToIndex, redirect.url);
|
|
582
|
+
const redirectStartTime = micros(redirect.ts - baseTime);
|
|
583
|
+
const redirectDuration = micros(redirect.dur);
|
|
584
|
+
return `[${urlIndex}|${redirectStartTime}|${redirectDuration}]`;
|
|
585
|
+
})
|
|
586
|
+
.join(',');
|
|
587
|
+
const initiators = this.#getInitiatorChain(parsedTrace, request);
|
|
588
|
+
const initiatorUrlIndices = initiators.map(initiator => this.#getOrAssignUrlIndex(urlIdToIndex, initiator.args.data.url));
|
|
589
|
+
const parts = [
|
|
590
|
+
urlIndex,
|
|
591
|
+
this.#eventsSerializer.keyForEvent(request) ?? '',
|
|
592
|
+
queuedTime,
|
|
593
|
+
requestSentTime,
|
|
594
|
+
downloadCompleteTime,
|
|
595
|
+
processingCompleteTime,
|
|
596
|
+
totalDuration,
|
|
597
|
+
downloadDuration,
|
|
598
|
+
mainThreadProcessingDuration,
|
|
599
|
+
statusCode,
|
|
600
|
+
mimeType,
|
|
601
|
+
priority,
|
|
602
|
+
initialPriority,
|
|
603
|
+
finalPriority,
|
|
604
|
+
renderBlocking,
|
|
605
|
+
protocol,
|
|
606
|
+
fromServiceWorker ? 't' : 'f',
|
|
607
|
+
initiatorUrlIndices.join(','),
|
|
608
|
+
`[${redirects}]`,
|
|
609
|
+
`[${headerValues ?? ''}]`,
|
|
610
|
+
];
|
|
611
|
+
return parts.join(';');
|
|
365
612
|
}
|
|
366
613
|
}
|
|
@@ -17,6 +17,9 @@ export class AICallTree {
|
|
|
17
17
|
selectedNode;
|
|
18
18
|
rootNode;
|
|
19
19
|
parsedTrace;
|
|
20
|
+
// Note: ideally this is passed in (or lived on ParsedTrace), but this class is
|
|
21
|
+
// stateless (mostly, there's a cache for some stuff) so it doesn't match much.
|
|
22
|
+
#eventsSerializer = new Trace.EventsSerializer.EventsSerializer();
|
|
20
23
|
constructor(selectedNode, rootNode, parsedTrace) {
|
|
21
24
|
this.selectedNode = selectedNode;
|
|
22
25
|
this.rootNode = rootNode;
|
|
@@ -262,7 +265,9 @@ export class AICallTree {
|
|
|
262
265
|
}
|
|
263
266
|
// 1. ID
|
|
264
267
|
const idStr = String(nodeId);
|
|
265
|
-
// 2.
|
|
268
|
+
// 2. eventKey
|
|
269
|
+
const eventKey = this.#eventsSerializer.keyForEvent(node.event);
|
|
270
|
+
// 3. Name
|
|
266
271
|
const name = Trace.Name.forEntry(event, parsedTrace);
|
|
267
272
|
// Round milliseconds to one decimal place, return empty string if zero/undefined
|
|
268
273
|
const roundToTenths = (num) => {
|
|
@@ -271,11 +276,11 @@ export class AICallTree {
|
|
|
271
276
|
}
|
|
272
277
|
return String(Math.round(num * 10) / 10);
|
|
273
278
|
};
|
|
274
|
-
//
|
|
279
|
+
// 4. Duration
|
|
275
280
|
const durationStr = roundToTenths(node.totalTime);
|
|
276
|
-
//
|
|
281
|
+
// 5. Self Time
|
|
277
282
|
const selfTimeStr = roundToTenths(node.selfTime);
|
|
278
|
-
//
|
|
283
|
+
// 6. URL Index
|
|
279
284
|
const url = SourceMapsResolver.SourceMapsResolver.resolvedURLForEntry(parsedTrace, event);
|
|
280
285
|
let urlIndexStr = '';
|
|
281
286
|
if (url) {
|
|
@@ -287,17 +292,18 @@ export class AICallTree {
|
|
|
287
292
|
urlIndexStr = String(existingIndex);
|
|
288
293
|
}
|
|
289
294
|
}
|
|
290
|
-
//
|
|
295
|
+
// 7. Child Range
|
|
291
296
|
const children = Array.from(node.children().values());
|
|
292
297
|
let childRangeStr = '';
|
|
293
298
|
if (childStartingNodeIndex) {
|
|
294
299
|
childRangeStr = (children.length === 1) ? String(childStartingNodeIndex) :
|
|
295
300
|
`${childStartingNodeIndex}-${childStartingNodeIndex + children.length}`;
|
|
296
301
|
}
|
|
297
|
-
//
|
|
302
|
+
// 8. Selected Marker
|
|
298
303
|
const selectedMarker = selectedNode?.event === node.event ? 'S' : '';
|
|
299
304
|
// Combine fields
|
|
300
305
|
let line = idStr;
|
|
306
|
+
line += ';' + eventKey;
|
|
301
307
|
line += ';' + name;
|
|
302
308
|
line += ';' + durationStr;
|
|
303
309
|
line += ';' + selfTimeStr;
|
|
@@ -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
|
import * as Trace from '../../../models/trace/trace.js';
|
|
5
|
+
import { AICallTree } from './AICallTree.js';
|
|
5
6
|
function getFirstInsightSet(insights) {
|
|
6
7
|
// Currently only support a single insight set. Pick the first one with a navigation.
|
|
7
8
|
// TODO(cjamcl): we should just give the agent the entire insight set, and give
|
|
@@ -9,7 +10,7 @@ function getFirstInsightSet(insights) {
|
|
|
9
10
|
return [...insights.values()].filter(insightSet => insightSet.navigation).at(0) ?? null;
|
|
10
11
|
}
|
|
11
12
|
export class AgentFocus {
|
|
12
|
-
static
|
|
13
|
+
static fromParsedTrace(parsedTrace) {
|
|
13
14
|
if (!parsedTrace.insights) {
|
|
14
15
|
throw new Error('missing insights');
|
|
15
16
|
}
|
|
@@ -17,6 +18,7 @@ export class AgentFocus {
|
|
|
17
18
|
return new AgentFocus({
|
|
18
19
|
parsedTrace,
|
|
19
20
|
insightSet,
|
|
21
|
+
event: null,
|
|
20
22
|
callTree: null,
|
|
21
23
|
insight: null,
|
|
22
24
|
});
|
|
@@ -29,10 +31,19 @@ export class AgentFocus {
|
|
|
29
31
|
return new AgentFocus({
|
|
30
32
|
parsedTrace,
|
|
31
33
|
insightSet,
|
|
34
|
+
event: null,
|
|
32
35
|
callTree: null,
|
|
33
36
|
insight,
|
|
34
37
|
});
|
|
35
38
|
}
|
|
39
|
+
static fromEvent(parsedTrace, event) {
|
|
40
|
+
if (!parsedTrace.insights) {
|
|
41
|
+
throw new Error('missing insights');
|
|
42
|
+
}
|
|
43
|
+
const insightSet = getFirstInsightSet(parsedTrace.insights);
|
|
44
|
+
const result = AgentFocus.#getCallTreeOrEvent(parsedTrace, event);
|
|
45
|
+
return new AgentFocus({ parsedTrace, insightSet, event: result.event, callTree: result.callTree, insight: null });
|
|
46
|
+
}
|
|
36
47
|
static fromCallTree(callTree) {
|
|
37
48
|
const insights = callTree.parsedTrace.insights;
|
|
38
49
|
// Select the insight set containing the call tree.
|
|
@@ -46,30 +57,76 @@ export class AgentFocus {
|
|
|
46
57
|
})) ??
|
|
47
58
|
getFirstInsightSet(insights);
|
|
48
59
|
}
|
|
49
|
-
return new AgentFocus({ parsedTrace: callTree.parsedTrace, insightSet, callTree, insight: null });
|
|
60
|
+
return new AgentFocus({ parsedTrace: callTree.parsedTrace, insightSet, event: null, callTree, insight: null });
|
|
50
61
|
}
|
|
51
62
|
#data;
|
|
63
|
+
eventsSerializer = new Trace.EventsSerializer.EventsSerializer();
|
|
52
64
|
constructor(data) {
|
|
53
65
|
this.#data = data;
|
|
54
66
|
}
|
|
55
|
-
get
|
|
56
|
-
return this.#data;
|
|
67
|
+
get parsedTrace() {
|
|
68
|
+
return this.#data.parsedTrace;
|
|
69
|
+
}
|
|
70
|
+
get insightSet() {
|
|
71
|
+
return this.#data.insightSet;
|
|
72
|
+
}
|
|
73
|
+
/** Note: at most one of event or callTree is non-null. */
|
|
74
|
+
get event() {
|
|
75
|
+
return this.#data.event;
|
|
76
|
+
}
|
|
77
|
+
/** Note: at most one of event or callTree is non-null. */
|
|
78
|
+
get callTree() {
|
|
79
|
+
return this.#data.callTree;
|
|
80
|
+
}
|
|
81
|
+
get insight() {
|
|
82
|
+
return this.#data.insight;
|
|
57
83
|
}
|
|
58
84
|
withInsight(insight) {
|
|
59
85
|
const focus = new AgentFocus(this.#data);
|
|
60
86
|
focus.#data.insight = insight;
|
|
61
87
|
return focus;
|
|
62
88
|
}
|
|
63
|
-
|
|
89
|
+
withEvent(event) {
|
|
64
90
|
const focus = new AgentFocus(this.#data);
|
|
65
|
-
|
|
91
|
+
const result = AgentFocus.#getCallTreeOrEvent(this.#data.parsedTrace, event);
|
|
92
|
+
focus.#data.callTree = result.callTree;
|
|
93
|
+
focus.#data.event = result.event;
|
|
66
94
|
return focus;
|
|
67
95
|
}
|
|
96
|
+
lookupEvent(key) {
|
|
97
|
+
try {
|
|
98
|
+
return this.eventsSerializer.eventForKey(key, this.#data.parsedTrace);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
if (err.toString().includes('Unknown trace event') || err.toString().includes('Unknown profile call')) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* If an event is a call tree, this returns that call tree and a null event.
|
|
109
|
+
* If not a call tree, this only returns a non-null event if the event is a network
|
|
110
|
+
* request.
|
|
111
|
+
* This is an arbitrary limitation – it should be removed, but first we need to
|
|
112
|
+
* improve the agent's knowledge of events that are not main-thread or network
|
|
113
|
+
* events.
|
|
114
|
+
*/
|
|
115
|
+
static #getCallTreeOrEvent(parsedTrace, event) {
|
|
116
|
+
const callTree = event && AICallTree.fromEvent(event, parsedTrace);
|
|
117
|
+
if (callTree) {
|
|
118
|
+
return { callTree, event: null };
|
|
119
|
+
}
|
|
120
|
+
if (event && Trace.Types.Events.isSyntheticNetworkRequest(event)) {
|
|
121
|
+
return { callTree: null, event };
|
|
122
|
+
}
|
|
123
|
+
return { callTree: null, event: null };
|
|
124
|
+
}
|
|
68
125
|
}
|
|
69
126
|
export function getPerformanceAgentFocusFromModel(model) {
|
|
70
127
|
const parsedTrace = model.parsedTrace();
|
|
71
128
|
if (!parsedTrace) {
|
|
72
129
|
return null;
|
|
73
130
|
}
|
|
74
|
-
return AgentFocus.
|
|
131
|
+
return AgentFocus.fromParsedTrace(parsedTrace);
|
|
75
132
|
}
|
package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/EventsSerializer.js
CHANGED
|
@@ -29,7 +29,7 @@ export class EventsSerializer {
|
|
|
29
29
|
if (EventsSerializer.isLegacyTimelineFrameKey(eventValues)) {
|
|
30
30
|
const event = parsedTrace.data.Frames.frames.at(eventValues.rawIndex);
|
|
31
31
|
if (!event) {
|
|
32
|
-
throw new Error(`Could not find frame with index ${eventValues.rawIndex}`);
|
|
32
|
+
throw new Error(`Unknown trace event. Could not find frame with index ${eventValues.rawIndex}`);
|
|
33
33
|
}
|
|
34
34
|
return event;
|
|
35
35
|
}
|
|
@@ -37,7 +37,7 @@ export class EventsSerializer {
|
|
|
37
37
|
const syntheticEvents = Helpers.SyntheticEvents.SyntheticEventsManager.getActiveManager().getSyntheticTraces();
|
|
38
38
|
const syntheticEvent = syntheticEvents.at(eventValues.rawIndex);
|
|
39
39
|
if (!syntheticEvent) {
|
|
40
|
-
throw new Error(`Attempted to get a synthetic event from an unknown raw event index: ${eventValues.rawIndex}`);
|
|
40
|
+
throw new Error(`Unknown trace event. Attempted to get a synthetic event from an unknown raw event index: ${eventValues.rawIndex}`);
|
|
41
41
|
}
|
|
42
42
|
return syntheticEvent;
|
|
43
43
|
}
|
|
@@ -45,7 +45,7 @@ export class EventsSerializer {
|
|
|
45
45
|
const rawEvents = Helpers.SyntheticEvents.SyntheticEventsManager.getActiveManager().getRawTraceEvents();
|
|
46
46
|
return rawEvents[eventValues.rawIndex];
|
|
47
47
|
}
|
|
48
|
-
throw new Error(`Unknown trace event
|
|
48
|
+
throw new Error(`Unknown trace event. Serializable key values: ${eventValues.join('-')}`);
|
|
49
49
|
}
|
|
50
50
|
static isProfileCallKey(key) {
|
|
51
51
|
return key.type === "p" /* Types.File.EventKeyType.PROFILE_CALL */;
|