UNPKG

chrome-devtools-frontend

Version:
180 lines (162 loc) 7.05 kB
// Copyright 2025 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Trace from '../../../models/trace/trace.js'; import {AICallTree} from './AICallTree.js'; /** * This class holds the Insight that is active when the user has entered the * Ask AI flow from the Insights sidebar. * Ideally we would just use the InsightModel instance itself, but we need to * also store a reference to the parsed trace as we use that to populate the * data provided to the LLM, so we use this class as a container for the insight * and the parsed trace. */ export class ActiveInsight { #insight: Trace.Insights.Types.InsightModel; #parsedTrace: Trace.Handlers.Types.ParsedTrace; constructor(insight: Trace.Insights.Types.InsightModel, parsedTrace: Trace.Handlers.Types.ParsedTrace) { this.#insight = insight; this.#parsedTrace = parsedTrace; } get insight(): Readonly<Trace.Insights.Types.InsightModel> { return this.#insight; } get parsedTrace(): Trace.Handlers.Types.ParsedTrace { return this.#parsedTrace; } title(): string { return this.#insight.title; } } export class AIQueries { /** * Returns the set of network requests that occurred within the timeframe of this Insight. */ static networkRequests(insight: Trace.Insights.Types.InsightModel, parsedTrace: Trace.Handlers.Types.ParsedTrace): readonly Trace.Types.Events.SyntheticNetworkRequest[] { const bounds = insightBounds(insight, parsedTrace); // Now we find network requests that: // 1. began within the bounds // 2. completed within the bounds const matchedRequests: Trace.Types.Events.SyntheticNetworkRequest[] = []; for (const request of parsedTrace.NetworkRequests.byTime) { // Requests are ordered by time ASC, so if we find one request that is // beyond the max, the rest are guaranteed to be also and we can break early. if (request.ts > bounds.max) { break; } if (request.args.data.url.startsWith('data:')) { // For the sake of the LLM querying data, we don't care about data: URLs. continue; } if (request.ts >= bounds.min && request.ts + request.dur <= bounds.max) { matchedRequests.push(request); } } return matchedRequests; } /** * Returns the single network request. We do not check to filter this by the * bounds of the insight, because the only way that the LLM has found this * request is by first inspecting a summary of relevant network requests for * the given insight. So if it then looks up a request by URL, we know that * is a valid and relevant request. */ static networkRequest(parsedTrace: Trace.Handlers.Types.ParsedTrace, url: string): Trace.Types.Events.SyntheticNetworkRequest|null { return parsedTrace.NetworkRequests.byTime.find(r => r.args.data.url === url) ?? null; } /** * Returns an AI Call Tree representing the activity on the main thread for * the relevant time range of the given insight. */ static mainThreadActivity(insight: Trace.Insights.Types.InsightModel, parsedTrace: Trace.Handlers.Types.ParsedTrace): AICallTree|null { /** * We cannot assume that there is one main thread as there are scenarios * where there can be multiple (see crbug.com/402658800) as an example. * Therefore we calculate the main thread by using the thread that the * Insight has been associated to. Most Insights relate to a navigation, so * in this case we can use the navigation's PID/TID as we know that will * have run on the main thread that we are interested in. * If we do not have a navigation, we fall back to looking for the first * thread we find that is of type MAIN_THREAD. * Longer term we should solve this at the Trace Engine level to avoid * look-ups like this; this is the work that is tracked in * crbug.com/402658800. */ let mainThreadPID: Trace.Types.Events.ProcessID|null = null; let mainThreadTID: Trace.Types.Events.ThreadID|null = null; if (insight.navigationId) { const navigation = parsedTrace.Meta.navigationsByNavigationId.get(insight.navigationId); if (navigation?.args.data?.isOutermostMainFrame) { mainThreadPID = navigation.pid; mainThreadTID = navigation.tid; } } const threads = Trace.Handlers.Threads.threadsInTrace(parsedTrace); const thread = threads.find(thread => { if (mainThreadPID && mainThreadTID) { return thread.pid === mainThreadPID && thread.tid === mainThreadTID; } return thread.type === Trace.Handlers.Threads.ThreadType.MAIN_THREAD; }); if (!thread) { return null; } const bounds = insightBounds(insight, parsedTrace); return AICallTree.fromTimeOnThread({ thread: { pid: thread.pid, tid: thread.tid, }, parsedTrace, bounds, }); } } /** * Calculates the time bounds for the given insight that are relevant. * If the insight is attached to a navigation, this will be the start of that * navigation through to either the next navigation, or the end of the trace. * For some insights we change the bounds; for LCP insights we treat the max * bound as LCP time, as anything that happens after that cannot have impacted * it. */ function insightBounds(insight: Trace.Insights.Types.InsightModel, parsedTrace: Trace.Handlers.Types.ParsedTrace): Trace.Types.Timing.TraceWindowMicro { const navigationStart = insight.navigationId ? parsedTrace.Meta.navigationsByNavigationId.get(insight.navigationId) : undefined; const minBound = navigationStart?.ts ?? parsedTrace.Meta.traceBounds.min; let maxBound = customMaxBoundForInsight(insight); if (!maxBound) { maxBound = parsedTrace.Meta.traceBounds.max; if (navigationStart) { const nextNavigation = getNextNavigation(navigationStart, parsedTrace); if (nextNavigation) { maxBound = nextNavigation.ts; } } } return Trace.Helpers.Timing.traceWindowFromMicroSeconds(minBound, maxBound); } /** * For a given navigation on the main frame, return the next navigation, if there was one. */ function getNextNavigation( navigation: Trace.Types.Events.NavigationStart, parsedTrace: Trace.Handlers.Types.ParsedTrace): Trace.Types.Events.NavigationStart|null { for (let i = 0; i < parsedTrace.Meta.mainFrameNavigations.length; i++) { const currentNavigationStart = parsedTrace.Meta.mainFrameNavigations[i]; if (currentNavigationStart.args.data?.navigationId === navigation.args.data?.navigationId) { return parsedTrace.Meta.mainFrameNavigations.at(i + 1) ?? null; } } return null; } function customMaxBoundForInsight(insight: Trace.Insights.Types.InsightModel): Trace.Types.Timing.Micro|null { if (Trace.Insights.Models.LCPPhases.isLCPPhases(insight) && insight.lcpEvent) { return insight.lcpEvent.ts; } return null; }