UNPKG

chrome-devtools-frontend

Version:
1,165 lines (1,001 loc) 59.7 kB
// Copyright 2024 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Common from '../../../core/common/common.js'; import * as Host from '../../../core/host/host.js'; import * as i18n from '../../../core/i18n/i18n.js'; import * as Platform from '../../../core/platform/platform.js'; import * as Root from '../../../core/root/root.js'; import * as SDK from '../../../core/sdk/sdk.js'; import * as Tracing from '../../../services/tracing/tracing.js'; import * as Annotations from '../../annotations/annotations.js'; import * as Logs from '../../logs/logs.js'; import * as SourceMapScopes from '../../source_map_scopes/source_map_scopes.js'; import * as Trace from '../../trace/trace.js'; import {ArtifactsManager} from '../ArtifactsManager.js'; import { PerformanceInsightFormatter, } from '../data_formatters/PerformanceInsightFormatter.js'; import {PerformanceTraceFormatter} from '../data_formatters/PerformanceTraceFormatter.js'; import {debugLog} from '../debug.js'; import {AICallTree} from '../performance/AICallTree.js'; import {AgentFocus} from '../performance/AIContext.js'; import { AiAgent, type ContextResponse, ConversationContext, type ConversationSuggestions, type FunctionCallHandlerResult, type ParsedResponse, type RequestOptions, type ResponseData, ResponseType, } from './AiAgent.js'; const UIStringsNotTranslated = { /** *@description Shown when the agent is investigating a trace */ analyzingTrace: 'Analyzing trace', /** * @description Shown when the agent is investigating network activity */ networkActivitySummary: 'Investigating network activity…', /** * @description Shown when the agent is investigating main thread activity */ mainThreadActivity: 'Investigating main thread activity…', } as const; const lockedString = i18n.i18n.lockedString; /** * WARNING: preamble defined in code is only used when userTier is * TESTERS. Otherwise, a server-side preamble is used (see * chrome_preambles.gcl). Sync local changes with the server-side. */ const greenDevAdditionalAnnotationsFunction = ` - CRITICAL: You also have access to functions called addElementAnnotation and addNeworkRequestAnnotation, which should be used to highlight elements and network requests (respectively).`; const greenDevAdditionalAnnotationsGuidelines = ` - CRITICAL: Each time an element or a network request is mentioned, you MUST ALSO call the functions addElementAnnotation (for an element) or addNeworkRequestAnnotation (for a network request). - CRITICAL: Don't add more than one annotation per element or network request. - These functions should be called as soon as you identify the entity that needs to be highlighted. - In addition to this, the addElementAnnotation function should always be called for the LCP element, if known. - The annotationMessage should be descriptive and relevant to why the element or network request is being highlighted. `; const getGreenDevAdditionalWidgetGuidelines = (): string => { // GreenDev is experimenting with multiple ways to display widget: // if widgetsFromFunctionCalls is true, then we use function calls to add widgets // otherwise we use ai-insight tags const widgetsFromFunctionCalls = true; if (widgetsFromFunctionCalls) { return ` - CRITICAL: You have access to three functions for adding rich, interactive widgets to your response: \`addInsightWidget\`, \`addNetworkRequestWidget\`, and \`addFlameChartWidget\`. You MUST use these functions whenever you refer to a corresponding entity. - **\`addInsightWidget({insightType: '...'})\`**: - **When to use**: Call this function every time you mention a specific performance insight (e.g., LCP, INP, CLS culprits). - **Purpose**: It embeds an interactive widget that provides a detailed breakdown and visualization of the insight. - **Example**: If you are explaining the causes of a poor LCP score, you MUST also call \`addInsightWidget({insightType: 'LCPBreakdown'})\`. This provides the user with the data to explore alongside your explanation. - **\`addNetworkRequestWidget({eventKey: '...'})\`**: - **When to use**: Call this function whenever you discuss a specific network request. - **Purpose**: It adds a widget displaying the full details of the network request, such as its timing, headers, and priority. - **Critical**: The eventKey should be the trace event key (only the number, no letters prefix or -) of that script's network request. - **Example**: If you identify a render-blocking script, you MUST also call \`addNetworkRequestWidget({eventKey: '...'})\` with the trace event key (only the number, no letters prefix or -) of that script's network request. - **\`addFlameChartWidget({start: ..., end: ...})\`**: - **When to use**: Call this function to highlight a specific time range within the trace, especially when discussing long tasks, specific events, or periods of high activity. - **Purpose**: It embeds a focused flame chart visualization for the given time range (in microseconds). - **Example**: If you find a long task that is blocking the main thread, you MUST also call \`addFlameChartWidget({start: 123456, end: 789012})\`. This provides the user with the data to explore alongside your explanation. - **General Rules**: - You MUST call these functions as soon as you identify the entity you are discussing. - Do NOT add more than one widget for the same insight, network request, or time range to avoid redundancy. `; } return ` - **Visualizing Insights**: When discussing the breakdown of specific metrics or a performance problem, you must render the appropriate Insight Overview component. Use these tags on a new line within your response: - For LCP breakdown: <ai-insight value="LCPBreakdown"> - For INP breakdown: <ai-insight value="INPBreakdown"> - For CLS culprits: <ai-insight value="CLSCulprits"> - For third parties: <ai-insight value="ThirdParties"> - For document latency: <ai-insight value="DocumentLatency"> - For DOM size: <ai-insight value="DOMSize"> - For duplicate JavaScript: <ai-insight value="DuplicatedJavaScript"> - For font display: <ai-insight value="FontDisplay"> - For forced reflow: <ai-insight value="ForcedReflow"> - For image delivery: <ai-insight value="ImageDelivery"> - For LCP discovery: <ai-insight value="LCPDiscovery"> - For legacy JavaScript: <ai-insight value="LegacyJavaScript"> - For network dependency tree: <ai-insight value="NetworkDependencyTree"> - For render blocking: <ai-insight value="RenderBlocking"> - For slow CSS selector: <ai-insight value="SlowCSSSelector"> - For viewport: <ai-insight value="Viewport"> - For modern HTTP: <ai-insight value="ModernHTTP"> - For cache: <ai-insight value="Cache"> - Do not place the <ai-insight> tag inside markdown code blocks (backticks). Output the tag directly as raw text. - **Visualizing Network Request Details**: When discussing a specific network request, represent its details in a structured widget for improved readability and focus. - Use this tag on a new line within your response, replacing \`EVENT_KEY\` (only the number, no letters prefix or -) with the actual trace event key: - For network event details: <network-request-widget value="EVENT_KEY"> - **Visualizing Flamechart**: When discussing an interesting part of the trace, represent its details in a structured widget for improved readability and focus. - Use this tag on a new line within your response, replacing "MIN_MICROSECONDS" and "MAX_MICROSECONDS" with the actual start and end times in microseconds: - For a flame chart of a specific time range: <flame-chart-widget start="MIN_MICROSECONDS" end="MAX_MICROSECONDS"> - CRITICAL: MIN_MICROSECONDS and MAX_MICROSECONDS must be within the flamechart bounds and in microseconds. - When you mention a specific performance event like LCP, INP, or a long task, you MUST also include a flamechart widget focused on the exact time range of that event. - This provides essential visual context to your explanation. - CRITICAL: Avoid Redundancy - When using insight or network request widgets, do not repeat details in the text response. - For example, for LCP, the phases like Time to First Byte will be part of the insight widget, so you must not state them in the text. This applies to other insights and network request timings. - Do not display any of the same widgets more than once. For example, if you have already displayed a network request widget for a specific event, do not display it again in the same response. `; }; /** * Preamble clocks in at ~1341 tokens. * The prose is around 4.5 chars per token. * The data can be as bad as 1.8 chars per token * * Check token length in https://aistudio.google.com/ */ const buildPreamble = (): string => { const greenDevEnabled = Boolean(Root.Runtime.hostConfig.devToolsGreenDevUi?.enabled); const annotationsEnabled = Annotations.AnnotationRepository.annotationsEnabled(); return `You are an assistant, expert in web performance and highly skilled with Chrome DevTools. Your primary goal is to provide actionable advice to web developers about their web page by using the Chrome Performance Panel and analyzing a trace. You may need to diagnose problems yourself, or you may be given direction for what to focus on by the user. You will be provided a summary of a trace: some performance metrics; the most critical network requests; a bottom-up call graph summary; and a brief overview of available insights. Each insight has information about potential performance issues with the page. Don't mention anything about an insight without first getting more data about it by calling \`getInsightDetails\`. You have many functions available to learn more about the trace. Use these to confirm hypotheses, or to further explore the trace when diagnosing performance issues. ${annotationsEnabled ? greenDevAdditionalAnnotationsFunction : ''} You will be given bounds representing a time range within the trace. Bounds include a min and a max time in microseconds. max is always bigger than min in a bounds. The 3 main performance metrics are: - LCP: "Largest Contentful Paint" - INP: "Interaction to Next Paint" - CLS: "Cumulative Layout Shift" Trace events referenced in the information given to you will be marked with an \`eventKey\`. For example: \`LCP element: <img src="..."> (eventKey: r-123, ts: 123456)\` You can use this key with \`getEventByKey\` to get more information about that trace event. For example: \`getEventByKey('r-123')\` You can also use this key with \`selectEventByKey\` to show the user a specific event ## Step-by-step instructions for debugging performance issues Note: if the user asks a specific question about the trace (such as "What is my LCP?", or "How many requests were render-blocking?", directly answer their question and skip starting a performance investigation. Otherwise, your task is to collaborate with the user to discover and resolve real performance issues. ### Step 1: Determine a performance problem to investigate - With help from the user, determine what performance problem to focus on. - If the user is not specific about what problem to investigate, help them by doing a high-level investigation yourself. Present to the user a few options with 1-sentence summaries. Mention what performance metrics each option impacts. Call as many functions and confirm the data thoroughly: never present an option without being certain it is a real performance issue. Don't suggest solutions yet. - Rank the options from most impactful to least impactful, and present them to the user in that order. - Don't present more than 5 options. - Once a performance problem has been identified for investigation, move on to step 2. ### Step 2: Suggest solutions - Suggest possible solutions to remedy the identified performance problem. Be as specific as possible, using data from the trace via the provided functions to back up everything you say. You should prefer specific solutions, but absent any specific solution you may suggest general solutions (such as from an insight's documentation links). - A good first step to discover solutions is to consider the insights, but you should also validate all potential advice by analyzing the trace until you are confident about the root cause of a performance issue. ## Guidelines - Use the provided functions to get detailed performance data. Prioritize functions that provide context relevant to the performance issue being investigated. - Before finalizing your advice, look over it and validate using any relevant functions. If something seems off, refine the advice before giving it to the user. - Do not rely on assumptions or incomplete information. Use the provided functions to get more data when needed. - Use the track summary functions to get high-level detail about portions of the trace. For the \`bounds\` parameter, default to using the bounds of the trace. Never specifically ask the user for a bounds. You can use more narrow bounds (such as the bounds relevant to a specific insight) when appropriate. Narrow the bounds given functions when possible. - Use \`getEventByKey\` to get data on a specific trace event. This is great for root-cause analysis or validating any assumptions. - Provide clear, actionable recommendations. Avoid technical jargon unless necessary, and explain any technical terms used. - If you see a generic task like "Task", "Evaluate script" or "(anonymous)" in the main thread activity, try to look at its children to see what actual functions are executed and refer to those. When referencing the main thread activity, be as specific as you can. Ensure you identify to the user relevant functions and which script they were defined in. Avoid referencing "Task", "Evaluate script" and "(anonymous)" nodes if possible and instead focus on their children. - Structure your response using markdown headings and bullet points for improved readability. - Be direct and to the point. Avoid unnecessary introductory phrases or filler content. Focus on delivering actionable advice efficiently. ${annotationsEnabled ? greenDevAdditionalAnnotationsGuidelines : ''} ${greenDevEnabled ? getGreenDevAdditionalWidgetGuidelines() : ''} ## Strict Constraints Adhere to the following critical requirements: - Never show bounds to the user. - Never show eventKey to the user. - Ensure your responses only use ms for time units. - Ensure numbers for time units are rounded to the nearest whole number. - Ensure comprehensive data retrieval through function calls to provide accurate and complete recommendations. - If the user asks a specific question about web performance that doesn't have anything to do with the trace, don't call any functions and be succinct in your answer. - Before suggesting changing the format of an image, consider what format it is already in. For example, if the mime type is image/webp, do not suggest to the user that the image is converted to WebP, as the image is already in that format. - Do not mention the functions you call to gather information about the trace (e.g., \`getEventByKey\`, \`getMainThreadTrackSummary\`) in your output. These are internal implementation details that should be hidden from the user. - Do not mention that you are an AI, or refer to yourself in the third person. You are simulating a performance expert. - If asked about sensitive topics (religion, race, politics, sexuality, gender, etc.), respond with: "My expertise is limited to website performance analysis. I cannot provide information on that topic.". - Do not provide answers on non-web-development topics, such as legal, financial, medical, or personal advice. `; }; const extraPreambleWhenNotExternal = `Additional notes: When referring to a trace event that has a corresponding \`eventKey\`, annotate your output using markdown link syntax. For example: - When referring to an event that is a long task: [Long task](#r-123) - When referring to a URL for which you know the eventKey of: [https://www.example.com](#s-1827) - Never show the eventKey (like "eventKey: s-1852"); instead, use a markdown link as described above. When asking the user to make a choice between multiple options, output a list of choices at the end of your text response. The format is \`SUGGESTIONS: ["suggestion1", "suggestion2", "suggestion3"]\`. This MUST start on a newline, and be a single line. `; const buildExtraPreambleWhenFreshTrace = (): string => { const annotationsEnabled = Annotations.AnnotationRepository.annotationsEnabled(); const greenDevAdditionalGuidelineFreshTrace = ` When referring to an element for which you know the nodeId, always call the function addElementAnnotation, specifying the id and an annotation reason. When referring to a network request for which you know the eventKey for, always call the function addNetworkRequestAnnotation, specifying the id and an annotation reason. - CRITICAL: Each time you add an annotating link you MUST ALSO call the function addElementAnnotation. - CRITICAL: Each time you describe an element or network request as being problematic you MUST call the function addElementAnnotation and specify an annotation reason. - CRITICAL: Each time you describe a network request as being problematic you MUST call the function addNetworkRequestAnnotation and specify an annotation reason. - CRITICAL: If you spot ANY of the following problems: - Render blocking elements/network requests. - Significant long task (especially on main thread). - Layout shifts (e.g. due to unsized images). ... then you MUST call addNetworkRequestAnnotation for ALL network requests and addaddElementAnnotation for all elements described in your conclusion. `; const extraPreambleWhenFreshTrace = `Additional notes: When referring to an element for which you know the nodeId, annotate your output using markdown link syntax: - For example, if nodeId is 23: [LCP element](#node-23) - This link will reveal the element in the Elements panel - Never mention node or nodeId when referring to the element, and especially not in the link text. - When referring to the LCP, it's useful to also mention what the LCP element is via its nodeId. Use the markdown link syntax to do so. ${annotationsEnabled ? greenDevAdditionalGuidelineFreshTrace : ''}`; return extraPreambleWhenFreshTrace; }; enum ScorePriority { REQUIRED = 3, CRITICAL = 2, DEFAULT = 1, } export class PerformanceTraceContext extends ConversationContext<AgentFocus> { static fromParsedTrace(parsedTrace: Trace.TraceModel.ParsedTrace): PerformanceTraceContext { return new PerformanceTraceContext(AgentFocus.fromParsedTrace(parsedTrace)); } static fromInsight(parsedTrace: Trace.TraceModel.ParsedTrace, insight: Trace.Insights.Types.InsightModel): PerformanceTraceContext { return new PerformanceTraceContext(AgentFocus.fromInsight(parsedTrace, insight)); } static fromCallTree(callTree: AICallTree): PerformanceTraceContext { return new PerformanceTraceContext(AgentFocus.fromCallTree(callTree)); } #focus: AgentFocus; external = false; constructor(focus: AgentFocus) { super(); this.#focus = focus; } override getOrigin(): string { const {min, max} = this.#focus.parsedTrace.data.Meta.traceBounds; return `trace-${min}-${max}`; } override getItem(): AgentFocus { return this.#focus; } override getTitle(): string { const focus = this.#focus; let url = focus.primaryInsightSet?.url; if (!url) { url = new URL(focus.parsedTrace.data.Meta.mainFrameURL); } const parts = [`Trace: ${url.hostname}`]; if (focus.insight) { parts.push(focus.insight.title); } if (focus.event) { parts.push(Trace.Name.forEntry(focus.event)); } if (focus.callTree) { const node = focus.callTree.selectedNode ?? focus.callTree.rootNode; parts.push(Trace.Name.forEntry(node.event)); } return parts.join(' – '); } /** * Presents the default suggestions that are shown when the user first clicks * "Ask AI". */ override async getSuggestions(): Promise<ConversationSuggestions|undefined> { const focus = this.#focus; if (focus.callTree) { return [ {title: 'What\'s the purpose of this work?', jslogContext: 'performance-default'}, {title: 'Where is time being spent?', jslogContext: 'performance-default'}, {title: 'How can I optimize this?', jslogContext: 'performance-default'}, ]; } if (focus.insight) { return new PerformanceInsightFormatter(focus, focus.insight).getSuggestions(); } const suggestions: ConversationSuggestions = [{title: 'What performance issues exist with my page?', jslogContext: 'performance-default'}]; const insightSet = focus.primaryInsightSet; if (insightSet) { const lcp = insightSet ? Trace.Insights.Common.getLCP(insightSet) : null; const cls = insightSet ? Trace.Insights.Common.getCLS(insightSet) : null; const inp = insightSet ? Trace.Insights.Common.getINP(insightSet) : null; const ModelHandlers = Trace.Handlers.ModelHandlers; const GOOD = Trace.Handlers.ModelHandlers.PageLoadMetrics.ScoreClassification.GOOD; if (lcp && ModelHandlers.PageLoadMetrics.scoreClassificationForLargestContentfulPaint(lcp.value) !== GOOD) { suggestions.push({title: 'How can I improve LCP?', jslogContext: 'performance-default'}); } if (inp && ModelHandlers.UserInteractions.scoreClassificationForInteractionToNextPaint(inp.value) !== GOOD) { suggestions.push({title: 'How can I improve INP?', jslogContext: 'performance-default'}); } if (cls && ModelHandlers.LayoutShifts.scoreClassificationForLayoutShift(cls.value) !== GOOD) { suggestions.push({title: 'How can I improve CLS?', jslogContext: 'performance-default'}); } // Add up to 3 suggestions from the top failing insights. const top3FailingInsightSuggestions = Object.values(insightSet.model) .filter(model => model.state !== 'pass') .map(model => new PerformanceInsightFormatter(focus, model).getSuggestions().at(-1)) .filter(suggestion => !!suggestion) .slice(0, 3); suggestions.push(...top3FailingInsightSuggestions); } return suggestions; } } // 16k Tokens * ~4 char per token. const MAX_FUNCTION_RESULT_BYTE_LENGTH = 16384 * 4; /** * One agent instance handles one conversation. Create a new agent * instance for a new conversation. */ export class PerformanceAgent extends AiAgent<AgentFocus> { #formatter: PerformanceTraceFormatter|null = null; #lastEventForEnhancedQuery: Trace.Types.Events.Event|undefined; #lastInsightForEnhancedQuery: Trace.Insights.Types.InsightModel|undefined; #hasShownAnalyzeTraceContext = false; /** * Cache of all function calls made by the agent. This allows us to include (as a * fact) every function call to conversation requests, allowing the AI to access * all the results rather than just the most recent. * * TODO(b/442392194): I'm not certain this is needed. I do see past function call * responses in "historical_contexts", though I think it isn't including any * parameters in the "functionCall" entries. * * The record key is the result of a function's displayInfoFromArgs. */ #functionCallCacheForFocus = new Map<AgentFocus, Record<string, Host.AidaClient.RequestFact>>(); #notExternalExtraPreambleFact: Host.AidaClient.RequestFact = { text: extraPreambleWhenNotExternal, metadata: {source: 'devtools', score: ScorePriority.CRITICAL} }; #freshTraceExtraPreambleFact: Host.AidaClient.RequestFact = { text: buildExtraPreambleWhenFreshTrace(), metadata: {source: 'devtools', score: ScorePriority.CRITICAL} }; #networkDataDescriptionFact: Host.AidaClient.RequestFact = { text: PerformanceTraceFormatter.networkDataFormatDescription, metadata: {source: 'devtools', score: ScorePriority.CRITICAL} }; #callFrameDataDescriptionFact: Host.AidaClient.RequestFact = { text: PerformanceTraceFormatter.callFrameDataFormatDescription, metadata: {source: 'devtools', score: ScorePriority.CRITICAL} }; #traceFacts: Host.AidaClient.RequestFact[] = []; get preamble(): string { return buildPreamble(); } get clientFeature(): Host.AidaClient.ClientFeature { return Host.AidaClient.ClientFeature.CHROME_PERFORMANCE_FULL_AGENT; } get userTier(): string|undefined { return Boolean(Root.Runtime.hostConfig.devToolsGreenDevUi?.enabled) ? 'TESTERS' : Root.Runtime.hostConfig.devToolsAiAssistancePerformanceAgent?.userTier; } get options(): RequestOptions { const temperature = Root.Runtime.hostConfig.devToolsAiAssistancePerformanceAgent?.temperature; const modelId = Root.Runtime.hostConfig.devToolsAiAssistancePerformanceAgent?.modelId; return { temperature, modelId, }; } async * handleContextDetails(context: ConversationContext<AgentFocus>|null): AsyncGenerator<ContextResponse, void, void> { if (!context) { return; } if (this.#hasShownAnalyzeTraceContext) { return; } yield { type: ResponseType.CONTEXT, title: lockedString(UIStringsNotTranslated.analyzingTrace), details: [ { title: 'Trace', text: this.#formatter?.formatTraceSummary() ?? '', }, ], }; this.#hasShownAnalyzeTraceContext = true; } #callTreeContextSet = new WeakSet(); #isFunctionResponseTooLarge(response: string): boolean { return response.length > MAX_FUNCTION_RESULT_BYTE_LENGTH; } /** * Sometimes the model will output URLs as plaintext; or a markdown link * where the link is the actual URL. This function transforms such output * to an eventKey link. * * A simple way to see when this gets utilized is: * 1. go to paulirish.com, record a trace * 2. say "What performance issues exist with my page?" * 3. then say "images" */ #parseForKnownUrls(response: string): string { const focus = this.context?.getItem(); if (!focus) { return response; } // Regex with two main parts, separated by | (OR): // 1. (\[(.*?)\]\((.*?)\)): Captures a full markdown link. // - Group 1: The whole link, e.g., "[text](url)" // - Group 2: The link text, e.g., "text" // - Group 3: The link destination, e.g., "url" // 2. (https?:\/\/[^\s<>()]+): Captures a standalone URL. // - Group 4: The standalone URL, e.g., "https://google.com" const urlRegex = /(\[(.*?)\]\((.*?)\))|(https?:\/\/[^\s<>()]+)/g; return response.replace(urlRegex, (match, markdownLink, linkText, linkDest, standaloneUrlText) => { if (markdownLink) { if (linkDest.startsWith('#')) { return match; } } const urlText = linkDest ?? standaloneUrlText; if (!urlText) { return match; } const request = focus.parsedTrace.data.NetworkRequests.byTime.find(request => request.args.data.url === urlText); if (!request) { return match; } const eventKey = focus.eventsSerializer.keyForEvent(request); if (!eventKey) { return match; } return `[${urlText}](#${eventKey})`; }); } #parseMarkdown(response: string): string { /** * Sometimes the LLM responds with code chunks that wrap a text based markdown response. * If this happens, we want to remove those before continuing. * See b/405054694 for more details. */ const FIVE_BACKTICKS = '`````'; if (response.startsWith(FIVE_BACKTICKS) && response.endsWith(FIVE_BACKTICKS)) { return response.slice(FIVE_BACKTICKS.length, -FIVE_BACKTICKS.length); } return response; } override parseTextResponse(response: string): ParsedResponse { const parsedResponse = super.parseTextResponse(response); parsedResponse.answer = this.#parseForKnownUrls(parsedResponse.answer); parsedResponse.answer = this.#parseMarkdown(parsedResponse.answer); return parsedResponse; } override async enhanceQuery(query: string, context: PerformanceTraceContext|null): Promise<string> { if (!context) { this.clearDeclaredFunctions(); return query; } this.clearDeclaredFunctions(); this.#declareFunctions(context); const focus = context.getItem(); const selected: string[] = []; if (focus.event) { const includeEventInfo = focus.event !== this.#lastEventForEnhancedQuery; this.#lastEventForEnhancedQuery = focus.event; if (includeEventInfo) { selected.push(`User selected an event ${this.#formatter?.serializeEvent(focus.event)}.\n\n`); } } if (focus.callTree) { // If this is a followup chat about the same call tree, don't include the call tree serialization again. // We don't need to repeat it and we'd rather have more the context window space. let contextString = ''; if (!this.#callTreeContextSet.has(focus.callTree)) { contextString = focus.callTree.serialize(); this.#callTreeContextSet.add(focus.callTree); } if (contextString) { selected.push(`User selected the following call tree:\n\n${contextString}\n\n`); } } if (focus.insight) { // We only need to add Insight info to a prompt when the context changes. For example: // User clicks Insight A. We need to send info on Insight A with the prompt. // User asks follow up question. We do not need to resend Insight A with the prompt. // User clicks Insight B. We now need to send info on Insight B with the prompt. // User clicks Insight A. We should resend the Insight info with the prompt. const includeInsightInfo = focus.insight !== this.#lastInsightForEnhancedQuery; this.#lastInsightForEnhancedQuery = focus.insight; if (includeInsightInfo) { selected.push(`User selected the ${focus.insight.insightKey} insight.\n\n`); } } if (!selected.length) { return query; } selected.push(`# User query\n\n${query}`); return selected.join(''); } override async * run(initialQuery: string, options: { selected: PerformanceTraceContext|null, signal?: AbortSignal, }): AsyncGenerator<ResponseData, void, void> { const focus = options.selected?.getItem(); // Clear any previous facts in case the user changed the active context. this.clearFacts(); if (options.selected && focus) { await this.#addFacts(options.selected); } yield* super.run(initialQuery, options); } #createFactForTraceSummary(): void { if (!this.#formatter) { return; } const text = this.#formatter.formatTraceSummary(); if (!text) { return; } this.#traceFacts.push( {text: `Trace summary:\n${text}`, metadata: {source: 'devtools', score: ScorePriority.REQUIRED}}); } async #createFactForCriticalRequests(): Promise<void> { if (!this.#formatter) { return; } const text = await this.#formatter.formatCriticalRequests(); if (!text) { return; } this.#traceFacts.push({ text, metadata: {source: 'devtools', score: ScorePriority.CRITICAL}, }); } async #createFactForMainThreadBottomUpSummary(): Promise<void> { if (!this.#formatter) { return; } const formatter = this.#formatter; const text = await formatter.formatMainThreadBottomUpSummary(); if (!text) { return; } this.#traceFacts.push({ text, metadata: {source: 'devtools', score: ScorePriority.CRITICAL}, }); } async #createFactForThirdPartySummary(): Promise<void> { if (!this.#formatter) { return; } const text = await this.#formatter.formatThirdPartySummary(); if (!text) { return; } this.#traceFacts.push({ text, metadata: {source: 'devtools', score: ScorePriority.CRITICAL}, }); } async #createFactForLongestTasks(): Promise<void> { if (!this.#formatter) { return; } const text = await this.#formatter.formatLongestTasks(); if (!text) { return; } this.#traceFacts.push({ text, metadata: {source: 'devtools', score: ScorePriority.CRITICAL}, }); } async #addFacts(context: PerformanceTraceContext): Promise<void> { const focus = context.getItem(); if (!context.external) { this.addFact(this.#notExternalExtraPreambleFact); } const isFresh = Tracing.FreshRecording.Tracker.instance().recordingIsFresh(focus.parsedTrace); if (isFresh) { this.addFact(this.#freshTraceExtraPreambleFact); } this.addFact(this.#callFrameDataDescriptionFact); this.addFact(this.#networkDataDescriptionFact); if (!this.#traceFacts.length) { const target = SDK.TargetManager.TargetManager.instance().primaryPageTarget(); if (!target) { throw new Error('missing target'); } this.#formatter = new PerformanceTraceFormatter(focus); this.#formatter.resolveFunctionCode = async (url: Platform.DevToolsPath.UrlString, line: number, column: number) => { if (!target) { return null; } return await SourceMapScopes.FunctionCodeResolver.getFunctionCodeFromLocation( target, url, line, column, {contextLength: 200, contextLineLength: 5, appendProfileData: true}); }; this.#createFactForTraceSummary(); await this.#createFactForCriticalRequests(); await this.#createFactForMainThreadBottomUpSummary(); await this.#createFactForThirdPartySummary(); await this.#createFactForLongestTasks(); } for (const fact of this.#traceFacts) { this.addFact(fact); } const cachedFunctionCalls = this.#functionCallCacheForFocus.get(focus); if (cachedFunctionCalls) { for (const fact of Object.values(cachedFunctionCalls)) { this.addFact(fact); } } } #cacheFunctionResult(focus: AgentFocus, key: string, result: string): void { const fact: Host.AidaClient.RequestFact = { text: `This is the result of calling ${key}:\n${result}`, metadata: {source: key, score: ScorePriority.DEFAULT}, }; const cache = this.#functionCallCacheForFocus.get(focus) ?? {}; cache[key] = fact; this.#functionCallCacheForFocus.set(focus, cache); } #declareFunctions(context: PerformanceTraceContext): void { const focus = context.getItem(); const {parsedTrace} = focus; this.declareFunction<{insightSetId: string, insightName: string}, {details: string}>('getInsightDetails', { description: 'Returns detailed information about a specific insight of an insight set. Use this before commenting on any specific issue to get more information.', parameters: { type: Host.AidaClient.ParametersTypes.OBJECT, description: '', nullable: false, properties: { insightSetId: { type: Host.AidaClient.ParametersTypes.STRING, description: 'The id for the specific insight set. Only use the ids given in the "Available insight sets" list.', nullable: false, }, insightName: { type: Host.AidaClient.ParametersTypes.STRING, description: 'The name of the insight. Only use the insight names given in the "Available insights" list.', nullable: false, } }, }, displayInfoFromArgs: params => { return { title: lockedString(`Investigating insight ${params.insightName}…`), action: `getInsightDetails('${params.insightSetId}', '${params.insightName}')` }; }, handler: async params => { debugLog('Function call: getInsightDetails', params); const insightSet = parsedTrace.insights?.get(params.insightSetId); if (!insightSet) { const valid = ([...parsedTrace.insights?.values() ?? []]) .map( insightSet => `id: ${insightSet.id}, url: ${insightSet.url}, bounds: ${ this.#formatter?.serializeBounds(insightSet.bounds)}`) .join('; '); return {error: `Invalid insight set id. Valid insight set ids are: ${valid}`}; } const insight = insightSet.model[params.insightName as keyof Trace.Insights.Types.InsightModels]; if (!insight) { const valid = Object.keys(insightSet.model).join(', '); return {error: `No insight available. Valid insight names are: ${valid}`}; } const details = new PerformanceInsightFormatter(focus, insight).formatInsight(); const key = `getInsightDetails('${params.insightSetId}', '${params.insightName}')`; this.#cacheFunctionResult(focus, key, details); return {result: {details}}; }, }); this.declareFunction<{eventKey: string}, {details: string}>('getEventByKey', { description: 'Returns detailed information about a specific event. Use the detail returned to validate performance issues, but do not tell the user about irrelevant raw data from a trace event.', parameters: { type: Host.AidaClient.ParametersTypes.OBJECT, description: '', nullable: false, properties: { eventKey: { type: Host.AidaClient.ParametersTypes.STRING, description: 'The key for the event.', nullable: false, } }, }, displayInfoFromArgs: params => { return {title: lockedString('Looking at trace event…'), action: `getEventByKey('${params.eventKey}')`}; }, handler: async params => { debugLog('Function call: getEventByKey', params); const event = focus.lookupEvent(params.eventKey as Trace.Types.File.SerializableKey); if (!event) { return {error: 'Invalid eventKey'}; } // TODO(b/425270067): Format in the same way that "Summary" detail tab does. const details = JSON.stringify(event); const key = `getEventByKey('${params.eventKey}')`; this.#cacheFunctionResult(focus, key, details); return {result: {details}}; }, }); const createBounds = (min: Trace.Types.Timing.Micro, max: Trace.Types.Timing.Micro): Trace.Types.Timing.TraceWindowMicro|null => { if (min > max) { return null; } const clampedMin = Math.max(min ?? 0, parsedTrace.data.Meta.traceBounds.min); const clampedMax = Math.min(max ?? Number.POSITIVE_INFINITY, parsedTrace.data.Meta.traceBounds.max); if (clampedMin > clampedMax) { return null; } return Trace.Helpers.Timing.traceWindowFromMicroSeconds( clampedMin as Trace.Types.Timing.Micro, clampedMax as Trace.Types.Timing.Micro); }; this.declareFunction<{min: Trace.Types.Timing.Micro, max: Trace.Types.Timing.Micro}, { summary: string, }>('getMainThreadTrackSummary', { description: 'Returns a summary of the main thread for the given bounds. The result includes a top-down summary, bottom-up summary, third-parties summary, and a list of related insights for the events within the given bounds.', parameters: { type: Host.AidaClient.ParametersTypes.OBJECT, description: '', nullable: false, properties: { min: { type: Host.AidaClient.ParametersTypes.INTEGER, description: 'The minimum time of the bounds, in microseconds', nullable: false, }, max: { type: Host.AidaClient.ParametersTypes.INTEGER, description: 'The maximum time of the bounds, in microseconds', nullable: false, }, }, }, displayInfoFromArgs: args => { return { title: lockedString(UIStringsNotTranslated.mainThreadActivity), action: `getMainThreadTrackSummary({min: ${args.min}, max: ${args.max}})` }; }, handler: async args => { debugLog('Function call: getMainThreadTrackSummary'); if (!this.#formatter) { throw new Error('missing formatter'); } const bounds = createBounds(args.min, args.max); if (!bounds) { return {error: 'invalid bounds'}; } const formatter = this.#formatter; const summary = await formatter.formatMainThreadTrackSummary(bounds); if (this.#isFunctionResponseTooLarge(summary)) { return { error: 'getMainThreadTrackSummary response is too large. Try investigating using other functions, or a more narrow bounds', }; } const byteCount = Platform.StringUtilities.countWtf8Bytes(summary); Host.userMetrics.performanceAIMainThreadActivityResponseSize(byteCount); const key = `getMainThreadTrackSummary({min: ${bounds.min}, max: ${bounds.max}})`; this.#cacheFunctionResult(focus, key, summary); return {result: {summary}}; }, }); this.declareFunction< {min: Trace.Types.Timing.Micro, max: Trace.Types.Timing.Micro}, {summary: string}>('getNetworkTrackSummary', { description: 'Returns a summary of the network for the given bounds.', parameters: { type: Host.AidaClient.ParametersTypes.OBJECT, description: '', nullable: false, properties: { min: { type: Host.AidaClient.ParametersTypes.INTEGER, description: 'The minimum time of the bounds, in microseconds', nullable: false, }, max: { type: Host.AidaClient.ParametersTypes.INTEGER, description: 'The maximum time of the bounds, in microseconds', nullable: false, }, }, }, displayInfoFromArgs: args => { return { title: lockedString(UIStringsNotTranslated.networkActivitySummary), action: `getNetworkTrackSummary({min: ${args.min}, max: ${args.max}})` }; }, handler: async args => { debugLog('Function call: getNetworkTrackSummary'); if (!this.#formatter) { throw new Error('missing formatter'); } const bounds = createBounds(args.min, args.max); if (!bounds) { return {error: 'invalid bounds'}; } const summary = this.#formatter.formatNetworkTrackSummary(bounds); if (this.#isFunctionResponseTooLarge(summary)) { return { error: 'getNetworkTrackSummary response is too large. Try investigating using other functions, or a more narrow bounds', }; } const byteCount = Platform.StringUtilities.countWtf8Bytes(summary); Host.userMetrics.performanceAINetworkSummaryResponseSize(byteCount); const key = `getNetworkTrackSummary({min: ${bounds.min}, max: ${bounds.max}})`; this.#cacheFunctionResult(focus, key, summary); return {result: {summary}}; }, }); this.declareFunction<{eventKey: string}, {callTree: string}>('getDetailedCallTree', { description: 'Returns a detailed call tree for the given main thread event.', parameters: { type: Host.AidaClient.ParametersTypes.OBJECT, description: '', nullable: false, properties: { eventKey: { type: Host.AidaClient.ParametersTypes.STRING, description: 'The key for the event.', nullable: false, }, }, }, displayInfoFromArgs: args => { return {title: lockedString('Looking at call tree…'), action: `getDetailedCallTree('${args.eventKey}')`}; }, handler: async args => { debugLog('Function call: getDetailedCallTree'); if (!this.#formatter) { throw new Error('missing formatter'); } const event = focus.lookupEvent(args.eventKey as Trace.Types.File.SerializableKey); if (!event) { return {error: 'Invalid eventKey'}; } const tree = AICallTree.fromEvent(event, parsedTrace); if (!tree) { return {error: 'No call tree found'}; } const formatter = this.#formatter; const callTree = await formatter.formatCallTree(tree); const key = `getDetailedCallTree(${args.eventKey})`; this.#cacheFunctionResult(focus, key, callTree); return {result: {callTree}}; }, }); if (Annotations.AnnotationRepository.annotationsEnabled()) { this.declareFunction<{ elementId: string, annotationMessage: string, }>('addElementAnnotation', { description: 'Adds a visual annotation in the Elements panel, attached to a node with the specific UID provided. Use it to highlight nodes in the Elements panel and provide contextual suggestions to the user related to their queries.', parameters: { type: Host.AidaClient.ParametersTypes.OBJECT, description: '', nullable: false, properties: { elementId: { type: Host.AidaClient.ParametersTypes.STRING, description: 'The UID of the element to annotate.', nullable: false, }, annotationMessage: { type: Host.AidaClient.ParametersTypes.STRING, description: 'The message the annotation should show to the user.', nullable: false, }, }, }, handler: async params => { return await this.addElementAnnotation(params.elementId, params.annotationMessage); }, }); this.declareFunction<{ eventKey: string, annotationMessage: string, }>('addNetworkRequestAnnotation', { description: 'Adds a visual annotation in the Network panel, attached to the request with the specific UID provided. ' + 'Use it to highlight requests in the Network panel and provide contextual suggestions to the user ' + 'related to their queries.', parameters: { type: Host.AidaClient.ParametersTypes.OBJECT, description: '', nullable: false, properties: { eventKey: { type: Host.AidaClient.ParametersTypes.STRING, description: 'The event key of the network request to annotate.', nullable: false, }, annotationMessage: { type: Host.AidaClient.ParametersTypes.STRING, description: 'The message the annotation should show to the user.', nullable: false, }, }, }, handler: async params => { return await this.addNetworkRequestAnnotation(params.eventKey, params.annotationMessage); }, }); } this.declareFunction<{scriptUrl: string, line: number, column: number}, {result: string}>('getFunctionCode', { description: 'Returns the code for a function defined at the given location. The result is annotated with the runtime performance of each line of code.', parameters: { type: Host.AidaClient.ParametersTypes.OBJECT, description: '', nullable: false, properties: { scriptUrl: { type: Host.AidaClient.ParametersTypes.STRING, description: 'The url of the function.', nullable: false, }, line: { type: Host.AidaClient.ParametersTypes.INTEGER, description: 'The line number where the function is defined.', nullable: false, }, column: { type: Host.AidaClient.ParametersTypes.INTEGER, description: 'The column number where the function is defined.', nullable: false, }, }, }, displayInfoFromArgs: args => { return { title: lockedString('Looking up function code…'), action: `getFunctionCode('${args.scriptUrl}', ${args.line}, ${args.column})` }; }, handler: async args => { debugLog('Function call: getFunctionCode'); if (args.line === undefined) { return {error: 'Missing arg: line'}; } if (args.column === undefined) { return {error: 'Missing arg: column'}; } if (!this.#formatter) { throw new Error('missing formatter'); } const target = SDK.TargetManager.TargetManager.instance().primaryPageTarget(); if (!target) { throw new Error('missing target'); } const url = args.scriptUrl as Platform.DevToolsPath.UrlString; const code = await this.#formatter.resolveFunctionCodeAtLocation(url, args.line, args.column); if (!code) { return {error: 'Could not find code'}; } const result = this.#formatter.formatFunctionCode(code); const key = `getFunctionCode('${args.scriptUrl}', ${args.line}, ${args.column})`; this.#cacheFunctionResult(focus, key, result); return {result: {result}}; }, }); const isFresh = Tracing.FreshRecording.Tracker.instance().recordingIsFresh(parsedTrace); const isTraceApp = Root.Runtime.Runtime.isTraceApp(); this.declareFunction<{url: string}, {content: string}>('getResourceContent', { description: 'Returns the content of the resource with the given url. Only use this for text resource types. This function is helpful