Skip to content

Commit 0bba616

Browse files
jackfranklinDevtools-frontend LUCI CQ
authored andcommitted
AI: add Perf Insights formatter
This CL continues the work to lay the foundations of the Performance Insights agent. Note that as of this CL the agent remains not usable and is not expected to provide much value. - Added `PerformanceInsightFormatter` class to format insight details, links, and descriptions. - Updated `PerformanceInsightsAgent` to use the new formatter. - Added `isLCPPhases` type guard to `LCPPhases.ts`. This allows us to take a generic insight and narrow the type down. Bug: 394552594 Change-Id: I8b514bdb6d6ca404eca4f168a30254c0fba7b0e8 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6276133 Reviewed-by: Alex Rudenko <[email protected]> Auto-Submit: Jack Franklin <[email protected]> Commit-Queue: Jack Franklin <[email protected]> Commit-Queue: Alex Rudenko <[email protected]>
1 parent e5c358e commit 0bba616

File tree

9 files changed

+295
-7
lines changed

9 files changed

+295
-7
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1202,6 +1202,7 @@ grd_files_debug_sources = [
12021202
"front_end/panels/ai_assistance/components/userActionRow.css.js",
12031203
"front_end/panels/ai_assistance/data_formatters/FileFormatter.js",
12041204
"front_end/panels/ai_assistance/data_formatters/NetworkRequestFormatter.js",
1205+
"front_end/panels/ai_assistance/data_formatters/PerformanceInsightFormatter.js",
12051206
"front_end/panels/ai_assistance/debug.js",
12061207
"front_end/panels/animation/AnimationGroupPreviewUI.js",
12071208
"front_end/panels/animation/AnimationScreenshotPopover.js",

front_end/models/trace/insights/LCPPhases.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ interface LCPPhases {
8585
renderDelay: Types.Timing.Milli;
8686
}
8787

88+
export function isLCPPhases(model: InsightModel<{}, {}>): model is LCPPhasesInsightModel {
89+
return model.insightKey === 'LCPPhases';
90+
}
8891
export type LCPPhasesInsightModel = InsightModel<typeof UIStrings, {
8992
lcpMs?: Types.Timing.Milli,
9093
lcpTs?: Types.Timing.Milli,
@@ -93,9 +96,6 @@ export type LCPPhasesInsightModel = InsightModel<typeof UIStrings, {
9396
lcpRequest?: Types.Events.SyntheticNetworkRequest,
9497
phases?: LCPPhases,
9598
}>;
96-
export function isLCPPhases(model: InsightModel<{}, {}>): model is LCPPhasesInsightModel {
97-
return model.title === UIStrings.title;
98-
}
9999

100100
function anyValuesNaN(...values: number[]): boolean {
101101
return values.some(v => Number.isNaN(v));

front_end/panels/ai_assistance/BUILD.gn

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ devtools_module("ai_assistance") {
3535
"components/UserActionRow.ts",
3636
"data_formatters/FileFormatter.ts",
3737
"data_formatters/NetworkRequestFormatter.ts",
38+
"data_formatters/PerformanceInsightFormatter.ts",
3839
"debug.ts",
3940
]
4041

@@ -100,12 +101,14 @@ ts_library("unittests") {
100101
"agents/NetworkAgent.test.ts",
101102
"agents/PatchAgent.test.ts",
102103
"agents/PerformanceAgent.test.ts",
104+
"agents/PerformanceInsightsAgent.test.ts",
103105
"agents/StylingAgent.test.ts",
104106
"components/ChatView.test.ts",
105107
"components/MarkdownRendererWithCodeBlock.test.ts",
106108
"components/UserActionRow.test.ts",
107109
"data_formatters/FileFormatter.test.ts",
108110
"data_formatters/NetworkRequestFormatter.test.ts",
111+
"data_formatters/PerformanceInsightFormatter.test.ts",
109112
]
110113

111114
deps = [
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Copyright 2025 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import type * as Common from '../../../core/common/common.js';
6+
import type * as Host from '../../../core/host/host.js';
7+
import * as Trace from '../../../models/trace/trace.js';
8+
import {mockAidaClient} from '../../../testing/AiAssistanceHelpers.js';
9+
import * as TimelineUtils from '../../timeline/utils/utils.js';
10+
import {
11+
InsightContext,
12+
PerformanceInsightFormatter,
13+
PerformanceInsightsAgent,
14+
ResponseType,
15+
} from '../ai_assistance.js';
16+
17+
const FAKE_LCP_MODEL = {
18+
insightKey: 'LCPPhases',
19+
strings: {},
20+
title: 'LCP by phase' as Common.UIString.LocalizedString,
21+
description: 'some description' as Common.UIString.LocalizedString,
22+
category: Trace.Insights.Types.InsightCategory.ALL,
23+
state: 'fail',
24+
} as const;
25+
const FAKE_PARSED_TRACE = {} as unknown as Trace.Handlers.Types.ParsedTrace;
26+
27+
describe('PerformanceInsightsAgent', () => {
28+
describe('handleContextDetails', () => {
29+
it('outputs the right context for the initial query from the user', async () => {
30+
const mockInsight = new TimelineUtils.InsightAIContext.ActiveInsight(FAKE_LCP_MODEL, FAKE_PARSED_TRACE);
31+
const context = new InsightContext(mockInsight);
32+
const agent = new PerformanceInsightsAgent({
33+
aidaClient: mockAidaClient([[{
34+
explanation: 'This is the answer',
35+
metadata: {
36+
rpcGlobalId: 123,
37+
}
38+
}]])
39+
});
40+
41+
const responses = await Array.fromAsync(agent.run('test', {selected: context}));
42+
assert.deepEqual(responses, [
43+
{
44+
type: ResponseType.USER_QUERY,
45+
query: 'test',
46+
imageInput: undefined,
47+
},
48+
{
49+
type: ResponseType.CONTEXT,
50+
title: 'LCP by phase',
51+
details: [
52+
// Note: these are placeholder values, see the TODO in
53+
// PerformanceInsightsAgent.
54+
{title: 'LCP by phase', text: 'LCP by phase'},
55+
],
56+
},
57+
{
58+
type: ResponseType.QUERYING,
59+
},
60+
{
61+
type: ResponseType.ANSWER,
62+
text: 'This is the answer',
63+
complete: true,
64+
suggestions: undefined,
65+
rpcId: 123,
66+
},
67+
]);
68+
});
69+
});
70+
describe('enhanceQuery', () => {
71+
it('adds the context to the query from the user', async () => {
72+
const agent = new PerformanceInsightsAgent({
73+
aidaClient: {} as Host.AidaClient.AidaClient,
74+
});
75+
76+
const mockInsight = new TimelineUtils.InsightAIContext.ActiveInsight(FAKE_LCP_MODEL, FAKE_PARSED_TRACE);
77+
const context = new InsightContext(mockInsight);
78+
const extraContext = new PerformanceInsightFormatter(mockInsight.insight).formatInsight();
79+
80+
const finalQuery = await agent.enhanceQuery('What is this?', context);
81+
const expected = `${extraContext}
82+
83+
# User request:
84+
What is this?`;
85+
86+
assert.strictEqual(finalQuery, expected);
87+
});
88+
});
89+
});

front_end/panels/ai_assistance/agents/PerformanceInsightsAgent.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@ import * as Host from '../../../core/host/host.js';
66
import type * as Lit from '../../../ui/lit/lit.js';
77
import type * as TimelineUtils from '../../timeline/utils/utils.js';
88
import * as PanelUtils from '../../utils/utils.js';
9+
import {PerformanceInsightFormatter} from '../data_formatters/PerformanceInsightFormatter.js';
910

1011
import {
1112
type AgentOptions as BaseAgentOptions,
1213
AgentType,
1314
AiAgent,
15+
type ContextDetail,
1416
type ContextResponse,
1517
ConversationContext,
1618
type RequestOptions,
1719
type ResponseData,
20+
ResponseType,
1821
} from './AiAgent.js';
1922

2023
/* clang-format off */
@@ -27,7 +30,7 @@ You will be told the following information about the Insight:
2730
- The 'Insight description' which helps you understand what the insight is for and what the user is hoping to understand.
2831
- 'Insight details' which will be additional context and information to help you understand what the insight is showing the user. Use this information to suggest opportunities to improve the performance.
2932
30-
You have a number of functions to get information about the page and its performance. Use these functions to help you gather information to perform the user's query. For every query it is important to understand the network activity and also the main thread activity.
33+
You will also be provided with external resources. Use these to ensure you give correct, accurate and up to date answers.
3134
3235
## Step-by-step instructions
3336
@@ -80,9 +83,17 @@ export class PerformanceInsightsAgent extends AiAgent<TimelineUtils.InsightAICon
8083
// eslint-disable-next-line no-unused-private-class-members
8184
#insight: ConversationContext<TimelineUtils.InsightAIContext.ActiveInsight>|undefined;
8285

83-
override handleContextDetails(_activeContext: ConversationContext<TimelineUtils.InsightAIContext.ActiveInsight>|null):
84-
AsyncGenerator<ContextResponse, void, void> {
85-
throw new Error('not implemented');
86+
override async *
87+
handleContextDetails(activeContext: ConversationContext<TimelineUtils.InsightAIContext.ActiveInsight>|null):
88+
AsyncGenerator<ContextResponse, void, void> {
89+
if (!activeContext) {
90+
return;
91+
}
92+
93+
const title = activeContext.getItem().title();
94+
// TODO: Provide proper text with useful context details.
95+
const titleDetail: ContextDetail = {title, text: title};
96+
yield {type: ResponseType.CONTEXT, title, details: [titleDetail]};
8697
}
8798

8899
override readonly type = AgentType.PERFORMANCE_INSIGHT;
@@ -105,6 +116,19 @@ export class PerformanceInsightsAgent extends AiAgent<TimelineUtils.InsightAICon
105116
// TODO: define the set of functions for the LLM.
106117
}
107118

119+
override async enhanceQuery(
120+
query: string,
121+
selectedInsight: ConversationContext<TimelineUtils.InsightAIContext.ActiveInsight>|null): Promise<string> {
122+
if (!selectedInsight) {
123+
return query;
124+
}
125+
const formatter = new PerformanceInsightFormatter(selectedInsight.getItem().insight);
126+
const extraQuery = `${formatter.formatInsight()}\n\n# User request:\n`;
127+
128+
const finalQuery = `${extraQuery}${query}`;
129+
return finalQuery;
130+
}
131+
108132
override async * run(initialQuery: string, options: {
109133
signal?: AbortSignal, selected: ConversationContext<TimelineUtils.InsightAIContext.ActiveInsight>|null,
110134
}): AsyncGenerator<ResponseData, void, void> {

front_end/panels/ai_assistance/ai_assistance.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ export * from './EvaluateAction.js';
2020
export * from './ExtensionScope.js';
2121
export * from './data_formatters/FileFormatter.js';
2222
export * from './data_formatters/NetworkRequestFormatter.js';
23+
export * from './data_formatters/PerformanceInsightFormatter.js';
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright 2025 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import {describeWithEnvironment} from '../../../testing/EnvironmentHelpers.js';
6+
import {getFirstOrError, getInsightOrError} from '../../../testing/InsightHelpers.js';
7+
import {TraceLoader} from '../../../testing/TraceLoader.js';
8+
import {PerformanceInsightFormatter} from '../ai_assistance.js';
9+
10+
describe('PerformanceInsightFormatter', () => {
11+
describeWithEnvironment('for LCP by Phase', () => {
12+
it('serializes the correct details', async function() {
13+
const {parsedTrace, insights} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz');
14+
assert.isOk(insights);
15+
const firstNav = getFirstOrError(parsedTrace.Meta.navigationsByNavigationId.values());
16+
const insight = getInsightOrError('LCPPhases', insights, firstNav);
17+
18+
const formatter = new PerformanceInsightFormatter(insight);
19+
const output = formatter.formatInsight();
20+
21+
const expected = `## Insight title: LCP by phase
22+
23+
## Insight Description:
24+
This insight is used to analyse the loading of the LCP resource and identify which of the 4 phases are contributing most to the delay in rendering the LCP element. For this insight it can be useful to get a list of all network requests that happened before the LCP time and look for slow requests. You can also look for main thread activity during the phases, in particular the load delay and render delay phases.
25+
26+
## External resources:
27+
- https://web.dev/articles/lcp
28+
- https://web.dev/articles/optimize-lcp
29+
30+
## Insight details:
31+
All time units given to you are in milliseconds.
32+
The actual LCP time is 129.21 ms;
33+
34+
We can break this time down into the 4 phases that combine to make up the LCP time:
35+
36+
- Time to first byte: 7.94 ms
37+
- Load delay: 33.16 ms
38+
- Load time: 14.70 ms
39+
- Render delay: 73.41 ms`;
40+
assert.strictEqual(output, expected);
41+
});
42+
});
43+
});
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// Copyright 2025 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
import * as i18n from '../../../core/i18n/i18n.js';
5+
import * as Trace from '../../../models/trace/trace.js';
6+
7+
function formatMilli(x: number|undefined): string {
8+
if (x === undefined) {
9+
return '';
10+
}
11+
return i18n.TimeUtilities.preciseMillisToString(x, 2);
12+
}
13+
14+
export class PerformanceInsightFormatter {
15+
#insight: Trace.Insights.Types.InsightModel<{}, {}>;
16+
constructor(insight: Trace.Insights.Types.InsightModel<{}, {}>) {
17+
this.#insight = insight;
18+
}
19+
20+
formatInsight(): string {
21+
const {title} = this.#insight;
22+
return `## Insight title: ${title}
23+
24+
## Insight Description:
25+
${this.#description()}
26+
27+
## External resources:
28+
${this.#links()}
29+
30+
## Insight details:
31+
${this.#details()}`;
32+
}
33+
34+
#details(): string {
35+
if (Trace.Insights.Models.LCPPhases.isLCPPhases(this.#insight)) {
36+
const {phases, lcpMs} = this.#insight;
37+
if (!lcpMs) {
38+
return '';
39+
}
40+
41+
return `All time units given to you are in milliseconds.
42+
The actual LCP time is ${formatMilli(lcpMs)};
43+
44+
We can break this time down into the 4 phases that combine to make up the LCP time:
45+
46+
- Time to first byte: ${formatMilli(phases?.ttfb)}
47+
- Load delay: ${formatMilli(phases?.loadDelay)}
48+
- Load time: ${formatMilli(phases?.loadTime)}
49+
- Render delay: ${formatMilli(phases?.renderDelay)}`;
50+
}
51+
return '';
52+
}
53+
54+
#links(): string {
55+
switch (this.#insight.insightKey) {
56+
case 'CLSCulprits':
57+
return '';
58+
case 'DocumentLatency':
59+
return '';
60+
case 'DOMSize':
61+
return '';
62+
case 'DuplicateJavaScript':
63+
return '';
64+
case 'FontDisplay':
65+
return '';
66+
case 'ForcedReflow':
67+
return '';
68+
case 'ImageDelivery':
69+
return '';
70+
case 'InteractionToNextPaint':
71+
return '';
72+
case 'LCPDiscovery':
73+
return '';
74+
case 'LCPPhases':
75+
return `- https://web.dev/articles/lcp
76+
- https://web.dev/articles/optimize-lcp`;
77+
case 'LongCriticalNetworkTree':
78+
return '';
79+
case 'RenderBlocking':
80+
return '';
81+
case 'SlowCSSSelector':
82+
return '';
83+
case 'ThirdParties':
84+
return '';
85+
case 'Viewport':
86+
return '';
87+
}
88+
}
89+
#description(): string {
90+
switch (this.#insight.insightKey) {
91+
case 'CLSCulprits':
92+
return '';
93+
case 'DocumentLatency':
94+
return '';
95+
case 'DOMSize':
96+
return '';
97+
case 'DuplicateJavaScript':
98+
return '';
99+
case 'FontDisplay':
100+
return '';
101+
case 'ForcedReflow':
102+
return '';
103+
case 'ImageDelivery':
104+
return '';
105+
case 'InteractionToNextPaint':
106+
return '';
107+
case 'LCPDiscovery':
108+
return '';
109+
case 'LCPPhases':
110+
return 'This insight is used to analyse the loading of the LCP resource and identify which of the 4 phases are contributing most to the delay in rendering the LCP element. For this insight it can be useful to get a list of all network requests that happened before the LCP time and look for slow requests. You can also look for main thread activity during the phases, in particular the load delay and render delay phases.';
111+
case 'LongCriticalNetworkTree':
112+
return '';
113+
case 'RenderBlocking':
114+
return '';
115+
case 'SlowCSSSelector':
116+
return '';
117+
case 'ThirdParties':
118+
return '';
119+
case 'Viewport':
120+
return '';
121+
}
122+
}
123+
}

front_end/panels/timeline/utils/InsightAIContext.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ export class ActiveInsight {
2121
this.#parsedTrace = parsedTrace;
2222
}
2323

24+
get insight(): Readonly<Trace.Insights.Types.InsightModel<{}, {}>> {
25+
return this.#insight;
26+
}
27+
2428
title(): string {
2529
return this.#insight.title;
2630
}

0 commit comments

Comments
 (0)