Skip to content

Commit 843d516

Browse files
Connor ClarkDevtools-frontend LUCI CQ
authored andcommitted
[AI] Use full trace agent for insights AI feature
This replaces the original insights agent with the new, "full" trace agent, and removes the support code that is no longer needed. * when providing context to the agent, instead of directly including the insights summary in the initial query via enhanceQuery, denote that the user just clicked a specific insight. The trace summary will have some details about the insight, and the agent reliably calls getInsightDetails to learn more. * ensure createBounds never produces invalid bounds. * stop giving so much data to BaseInsightComponent to support the AI feature. Instead, just give it an agent focus directly. Bug: 442392194 Change-Id: Id3e03a7f66b672684c1e21d39dcc65aa919bd346 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6914156 Commit-Queue: Paul Irish <paulirish@chromium.org> Reviewed-by: Jack Franklin <jacktfranklin@chromium.org>
1 parent 96ddcac commit 843d516

11 files changed

Lines changed: 197 additions & 755 deletions

File tree

front_end/core/host/AidaClient.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,12 @@ export enum ClientFeature {
131131
CHROME_FILE_AGENT = 9,
132132
// Chrome AI Patch Agent.
133133
CHROME_PATCH_AGENT = 12,
134-
// Chrome AI Assistance Performance Insights Agent.
135-
CHROME_PERFORMANCE_INSIGHTS_AGENT = 13,
136134
// Chrome AI Assistance Performance Agent.
137135
CHROME_PERFORMANCE_FULL_AGENT = 24,
136+
137+
// Removed features (for reference).
138+
// Chrome AI Assistance Performance Insights Agent.
139+
// CHROME_PERFORMANCE_INSIGHTS_AGENT = 13,
138140
}
139141

140142
export enum UserTier {

front_end/models/ai_assistance/agents/PerformanceAgent.test.ts

Lines changed: 89 additions & 249 deletions
Large diffs are not rendered by default.

front_end/models/ai_assistance/agents/PerformanceAgent.ts

Lines changed: 67 additions & 332 deletions
Large diffs are not rendered by default.

front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export class PerformanceTraceFormatter {
1818
constructor(
1919
formatters: UnitFormatters, focus: TimelineUtils.AIContext.AgentFocus,
2020
eventsSerializer: Trace.EventsSerializer.EventsSerializer) {
21-
if (focus.data.type !== 'full') {
21+
if (focus.data.type !== 'full' && focus.data.type !== 'insight') {
2222
throw new Error('unexpected agent focus');
2323
}
2424

front_end/panels/ai_assistance/AiAssistancePanel.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ describeWithMockConnection('AI Assistance Panel', () => {
283283
flavor: TimelineUtils.AIContext.AgentFocus,
284284
createContext: () => {
285285
// @ts-expect-error: don't need any data.
286-
const context = AiAssistanceModel.PerformanceTraceContext.fromInsight(null, null, null);
286+
const context = AiAssistanceModel.PerformanceTraceContext.fromInsight(null, new Map());
287287
sinon.stub(AiAssistanceModel.PerformanceTraceContext.prototype, 'getSuggestions')
288288
.returns(Promise.resolve([{title: 'test suggestion'}]));
289289
return context;

front_end/panels/timeline/components/SidebarSingleInsightSet.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import * as ComponentHelpers from '../../../ui/components/helpers/helpers.js';
1212
import * as Lit from '../../../ui/lit/lit.js';
1313
import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js';
1414
import {md} from '../utils/Helpers.js';
15+
import * as Utils from '../utils/utils.js';
1516

1617
import type {BaseInsightComponent} from './insights/BaseInsightComponent.js';
1718
import {shouldRenderForCategory} from './insights/Helpers.js';
@@ -426,6 +427,12 @@ export class SidebarSingleInsightSet extends HTMLElement {
426427

427428
const renderInsightComponent = (insightData: CategorizedInsightData): Lit.TemplateResult => {
428429
const {componentClass, model} = insightData;
430+
if (!this.#data.parsedTrace || !this.#data.insights || !this.#data.traceMetadata) {
431+
return html``;
432+
}
433+
434+
const agentFocus = Utils.AIContext.AgentFocus.fromInsight(
435+
this.#data.parsedTrace, this.#data.insights, this.#data.traceMetadata, model);
429436
// clang-format off
430437
return html`<div>
431438
<${componentClass.litTagName}
@@ -438,7 +445,7 @@ export class SidebarSingleInsightSet extends HTMLElement {
438445
.model=${model}
439446
.bounds=${insightSet.bounds}
440447
.insightSetKey=${insightSetKey}
441-
.parsedTrace=${this.#data.parsedTrace}
448+
.agentFocus=${agentFocus}
442449
.fieldMetrics=${fieldMetrics}>
443450
</${componentClass.litTagName}>
444451
</div>`;

front_end/panels/timeline/components/insights/BaseInsightComponent.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,6 @@ describeWithEnvironment('BaseInsightComponent', () => {
212212
});
213213

214214
describe('Ask AI Insights', () => {
215-
const FAKE_PARSED_TRACE = {} as unknown as Trace.Handlers.Types.ParsedTrace;
216215
const FAKE_LCP_MODEL = {
217216
insightKey: 'LCPBreakdown',
218217
strings: {},
@@ -231,7 +230,6 @@ describeWithEnvironment('BaseInsightComponent', () => {
231230
component.selected = true;
232231
component.model = FAKE_LCP_MODEL;
233232
// We don't need a real trace for these tests.
234-
component.parsedTrace = FAKE_PARSED_TRACE;
235233
component.bounds = FAKE_INSIGHT_SET_BOUNDS;
236234
renderElementIntoDOM(component);
237235

@@ -315,6 +313,8 @@ describeWithEnvironment('BaseInsightComponent', () => {
315313
});
316314

317315
it('sets the context when the user clicks the button', async () => {
316+
const focus =
317+
new Utils.AIContext.AgentFocus({type: 'insight'} as unknown as Utils.AIContext.AgentFocusDataInsight);
318318
updateHostConfig({
319319
aidaAvailability: {
320320
enabled: true,
@@ -325,6 +325,7 @@ describeWithEnvironment('BaseInsightComponent', () => {
325325
}
326326
});
327327
const component = await renderComponent({insightHasAISupport: true});
328+
component.agentFocus = focus;
328329
assert.isOk(component.shadowRoot);
329330
const button = component.shadowRoot.querySelector('devtools-button[data-insights-ask-ai]');
330331
assert.isOk(button);
@@ -343,9 +344,11 @@ describeWithEnvironment('BaseInsightComponent', () => {
343344
});
344345

345346
it('clears the active context when it gets toggled shut', async () => {
346-
const focus = {data: {type: 'insight'}} as unknown as Utils.AIContext.AgentFocus;
347+
const focus =
348+
new Utils.AIContext.AgentFocus({type: 'insight'} as unknown as Utils.AIContext.AgentFocusDataInsight);
347349
UI.Context.Context.instance().setFlavor(Utils.AIContext.AgentFocus, focus);
348350
const component = await renderComponent({insightHasAISupport: true});
351+
component.agentFocus = focus;
349352
const header = component.shadowRoot?.querySelector('header');
350353
assert.isOk(header);
351354
dispatchClickEvent(header);

front_end/panels/timeline/components/insights/BaseInsightComponent.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export abstract class BaseInsightComponent<T extends InsightModel> extends HTMLE
8686

8787
#selected = false;
8888
#model: T|null = null;
89-
#parsedTrace: Trace.Handlers.Types.ParsedTrace|null = null;
89+
#agentFocus: Utils.AIContext.AgentFocus|null = null;
9090
#fieldMetrics: Trace.Insights.Common.CrUXFieldMetricResults|null = null;
9191

9292
get model(): T|null {
@@ -158,8 +158,8 @@ export abstract class BaseInsightComponent<T extends InsightModel> extends HTMLE
158158
void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render);
159159
}
160160

161-
set parsedTrace(parsedTrace: Trace.Handlers.Types.ParsedTrace) {
162-
this.#parsedTrace = parsedTrace;
161+
set agentFocus(agentFocus: Utils.AIContext.AgentFocus) {
162+
this.#agentFocus = agentFocus;
163163
}
164164

165165
set fieldMetrics(fieldMetrics: Trace.Insights.Common.CrUXFieldMetricResults) {
@@ -349,7 +349,7 @@ export abstract class BaseInsightComponent<T extends InsightModel> extends HTMLE
349349
}
350350

351351
#askAIButtonClick(): void {
352-
if (!this.#model || !this.#parsedTrace || !this.data.bounds) {
352+
if (!this.#agentFocus) {
353353
return;
354354
}
355355

@@ -359,8 +359,7 @@ export abstract class BaseInsightComponent<T extends InsightModel> extends HTMLE
359359
return;
360360
}
361361

362-
const context = Utils.AIContext.AgentFocus.fromInsight(this.#parsedTrace, this.#model, this.data.bounds);
363-
UI.Context.Context.instance().setFlavor(Utils.AIContext.AgentFocus, context);
362+
UI.Context.Context.instance().setFlavor(Utils.AIContext.AgentFocus, this.#agentFocus);
364363

365364
// Trigger the AI Assistance panel to open.
366365
const action = UI.ActionRegistry.ActionRegistry.instance().getAction(actionId);

front_end/panels/timeline/utils/AIContext.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@ interface AgentFocusDataCallTree {
1919
callTree: AICallTree;
2020
}
2121

22-
interface AgentFocusDataInsight {
22+
export interface AgentFocusDataInsight {
2323
type: 'insight';
2424
parsedTrace: Trace.Handlers.Types.ParsedTrace;
25+
insightSet: Trace.Insights.Types.InsightSet|null;
26+
traceMetadata: Trace.Types.File.MetaData;
2527
insight: Trace.Insights.Types.InsightModel;
26-
insightSetBounds: Trace.Types.Timing.TraceWindowMicro;
2728
}
2829

2930
type AgentFocusData = AgentFocusDataCallTree|AgentFocusDataInsight|AgentFocusDataFull;
@@ -43,13 +44,16 @@ export class AgentFocus {
4344
}
4445

4546
static fromInsight(
46-
parsedTrace: Trace.Handlers.Types.ParsedTrace, insight: Trace.Insights.Types.InsightModel,
47-
insightSetBounds: Trace.Types.Timing.TraceWindowMicro): AgentFocus {
47+
parsedTrace: Trace.Handlers.Types.ParsedTrace, insights: Trace.Insights.Types.TraceInsightSets,
48+
traceMetadata: Trace.Types.File.MetaData, insight: Trace.Insights.Types.InsightModel): AgentFocus {
49+
// Currently only support a single insight set. Pick the first one with a navigation.
50+
const insightSet = [...insights.values()].filter(insightSet => insightSet.navigation).at(0) ?? null;
4851
return new AgentFocus({
4952
type: 'insight',
5053
parsedTrace,
54+
insightSet,
55+
traceMetadata,
5156
insight,
52-
insightSetBounds,
5357
});
5458
}
5559

front_end/panels/timeline/utils/InsightAIContext.test.ts

Lines changed: 7 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -2,119 +2,24 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import * as Trace from '../../../models/trace/trace.js';
65
import {describeWithEnvironment} from '../../../testing/EnvironmentHelpers.js';
7-
import {getFirstOrError, getInsightOrError, getInsightSetOrError} from '../../../testing/InsightHelpers.js';
6+
import {getFirstOrError, getInsightSetOrError} from '../../../testing/InsightHelpers.js';
87
import {TraceLoader} from '../../../testing/TraceLoader.js';
98

109
import * as Utils from './utils.js';
1110

1211
describeWithEnvironment('AIQueries', () => {
13-
it('can query for network events relevant to the given insight', async function() {
12+
it('can query for the longest tasks', async function() {
1413
const {parsedTrace, insights} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz');
1514
assert.isOk(insights);
16-
const firstNav = getFirstOrError(parsedTrace.Meta.navigationsByNavigationId.values());
17-
const insightSet = getInsightSetOrError(insights, firstNav);
18-
const insight = getInsightOrError('LCPBreakdown', insights, firstNav);
19-
20-
const requests = Utils.InsightAIContext.AIQueries.networkRequests(insight, insightSet.bounds, parsedTrace);
21-
const expected = [
22-
'https://web.dev/',
23-
'https://web.dev/css/next.css?v=013a61aa',
24-
'https://web.dev/fonts/material-icons/regular.woff2',
25-
'https://web.dev/fonts/google-sans/regular/latin.woff2',
26-
'https://web.dev/fonts/google-sans/bold/latin.woff2',
27-
'https://web-dev.imgix.net/image/jxu1OdD7LKOGIDU7jURMpSH2lyK2/zrBPJq27O4Hs8haszVnK.svg',
28-
'https://web-dev.imgix.net/image/kheDArv5csY6rvQUJDbWRscckLr1/4i7JstVZvgTFk9dxCe4a.svg',
29-
'https://web-dev.imgix.net/image/jL3OLOhcWUQDnR4XjewLBx4e3PC3/3164So5aDk7vKTkhx9Vm.png?auto=format&w=1140',
30-
'https://web.dev/js/app.js?v=fedf5fbe',
31-
'https://web.dev/js/home.js?v=73b0d143',
32-
'https://web.dev/js/index-7e29abb6.js',
33-
'https://web.dev/js/index-578d2db7.js',
34-
'https://web.dev/js/index-f45448ab.js',
35-
'https://web.dev/js/actions-f0eb5c8e.js',
36-
];
37-
assert.deepEqual(requests.map(r => r.args.data.url), expected);
38-
});
39-
40-
it('correctly calculates the bounds when there are multiple navigations', async function() {
41-
const {parsedTrace, insights} = await TraceLoader.traceEngine(this, 'multiple-navigations-render-blocking.json.gz');
42-
assert.isOk(insights);
43-
const [firstNav, secondNav] = parsedTrace.Meta.mainFrameNavigations;
44-
assert.isOk(firstNav);
45-
assert.isOk(secondNav);
46-
const insightSet1 = getInsightSetOrError(insights, firstNav);
47-
const insightSet2 = getInsightSetOrError(insights, secondNav);
48-
const lcpNav1 = getInsightOrError('LCPBreakdown', insights, firstNav);
49-
const lcpNav2 = getInsightOrError('LCPBreakdown', insights, secondNav);
5015

51-
const requests1 = Utils.InsightAIContext.AIQueries.networkRequests(lcpNav1, insightSet1.bounds, parsedTrace);
52-
const requests2 = Utils.InsightAIContext.AIQueries.networkRequests(lcpNav2, insightSet2.bounds, parsedTrace);
53-
54-
// Both navigations load the same page, so we expect the set of URLs to be the same.
55-
const expected = ['http://localhost:8080/render-blocking', 'http://localhost:8080/render-blocking/script.js'];
56-
assert.deepEqual(requests1.map(r => r.args.data.url), expected);
57-
assert.deepEqual(requests2.map(r => r.args.data.url), expected);
58-
59-
// But we can check that the requests are not equal to each other,
60-
// and that we got the right ones for each insight.
61-
// For the first Insight requests, they all happen before the second navigation.
62-
assert.isTrue(requests1.every(req => req.ts < secondNav.ts));
63-
// For the second Insight requests, they all happen after second navigation
64-
assert.isTrue(requests2.every(req => req.ts > secondNav.ts));
65-
});
66-
67-
it('can query for main thread activity for an insight', async function() {
68-
const {parsedTrace, insights} = await TraceLoader.traceEngine(this, 'lcp-discovery-delay.json.gz');
69-
assert.isOk(insights);
7016
const firstNav = getFirstOrError(parsedTrace.Meta.navigationsByNavigationId.values());
7117
const insightSet = getInsightSetOrError(insights, firstNav);
72-
const insight = getInsightOrError('LCPBreakdown', insights, firstNav);
73-
const activity =
74-
Utils.InsightAIContext.AIQueries.mainThreadActivityForInsight(insight, insightSet.bounds, parsedTrace);
75-
assert.instanceOf(activity, Utils.AICallTree.AICallTree);
76-
// There are a few smaller tasks but for this test we want to make sure we
77-
// found the long task of ~999ms.
78-
const rootNode = activity.rootNode;
79-
const children = Array.from(rootNode.children().values()).map(n => n.event);
80-
const longTaskDuration = Trace.Types.Timing.Micro(999544);
81-
assert.isTrue(children.some(event => event.dur === longTaskDuration));
82-
});
83-
84-
it('limits the time bounds for DocumentRequestLatency to the timestamp of the document request', async function() {
85-
const {parsedTrace, insights} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz');
86-
assert.isOk(insights);
87-
const firstNav = getFirstOrError(parsedTrace.Meta.navigationsByNavigationId.values());
88-
const insightSet = getInsightSetOrError(insights, firstNav);
89-
const insight = getInsightOrError('DocumentLatency', insights, firstNav);
90-
91-
const requests = Utils.InsightAIContext.AIQueries.networkRequests(insight, insightSet.bounds, parsedTrace);
92-
assert.isOk(insight.data?.documentRequest);
93-
// The only relevant request is the document request itself.
94-
assert.deepEqual(requests, [insight.data.documentRequest]);
95-
});
96-
97-
it('limits the trace bounds for an INP insight to just the interaction', async function() {
98-
const {parsedTrace, insights} = await TraceLoader.traceEngine(this, 'slow-interaction-keydown.json.gz');
99-
assert.isOk(insights);
100-
const insightSet = getInsightSetOrError(insights);
101-
const insight = getInsightOrError('INPBreakdown', insights);
102-
103-
const activity =
104-
Utils.InsightAIContext.AIQueries.mainThreadActivityForInsight(insight, insightSet.bounds, parsedTrace);
105-
assert.isOk(activity);
106-
107-
// These are the first 3 nodes that we expect. The structure of the
108-
// timeline under the long keydown interaction are:
109-
// X YYYYYYYYYYYYYYY
110-
// A ..............
111-
// Where X = Node 1 below, A = Node 2 below, and YYYYYYY is the long
112-
// interaction = Node 3 below.
113-
const expectedToContain = `# Call tree:
18+
const tasks =
19+
Utils.InsightAIContext.AIQueries.longestTasks(firstNav.args.data?.navigationId, insightSet.bounds, parsedTrace);
20+
assert.isOk(tasks);
11421

115-
1;Task;1.1;0.2;;3
116-
2;Task;143.2;0.1;;4
117-
3;Event: keydown;1;1;;`;
118-
assert.include(activity.serialize(), expectedToContain);
22+
const expected = [33, 21, 16];
23+
assert.deepEqual(tasks.map(task => Math.round(task.rootNode.totalTime)), expected);
11924
});
12025
});

0 commit comments

Comments
 (0)