Skip to content

Commit 2f91327

Browse files
jackfranklinDevtools-frontend LUCI CQ
authored andcommitted
AI: provide network summary to Perf Insights agent
Someone found a bug where their site broke the agent because it had 180 requests that the agent asked for details of and this quickly ate up the available context window. This CL changes the architecture so the LLM is expected to first get a summary of the network requests before then being allowed to enquire about specific requests. There is definitely more room to experiment here; we also could provide the LLM with a list of "critical" requests based on heuristics we have, rather than providing a list of all requests. This needs more experimentation. Fixed: 400909537 Change-Id: I0d224834010dad050411a0024d226ca1e22f3664 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6324575 Auto-Submit: Jack Franklin <[email protected]> Reviewed-by: Alex Rudenko <[email protected]> Commit-Queue: Jack Franklin <[email protected]>
1 parent 2380d43 commit 2f91327

File tree

6 files changed

+144
-20
lines changed

6 files changed

+144
-20
lines changed

front_end/panels/ai_assistance/AiAssistancePanel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ function getEmptyStateSuggestions(conversationType: ConversationType): string[]
253253
];
254254
case ConversationType.PERFORMANCE_INSIGHT:
255255
// TODO(b/393061683): Define these.
256-
return ['Placeholder', 'Suggestions', 'For now'];
256+
return ['Help me optimize my LCP', 'Suggestions', 'For now'];
257257
}
258258
}
259259

front_end/panels/ai_assistance/agents/PerformanceInsightsAgent.test.ts

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,15 @@ What is this?`;
9595
});
9696

9797
describe('function calls', () => {
98-
it('calls getNetworkActivity', async function() {
98+
it('calls getNetworkActivitySummary', async function() {
9999
const {parsedTrace, insights} = await TraceLoader.traceEngine(this, 'lcp-images.json.gz');
100100
assert.isOk(insights);
101101
const [firstNav] = parsedTrace.Meta.mainFrameNavigations;
102102
const lcpPhases = getInsightOrError('LCPPhases', insights, firstNav);
103103
const agent = new PerformanceInsightsAgent({
104-
aidaClient: mockAidaClient(
105-
[[{explanation: '', functionCalls: [{name: 'getNetworkActivity', args: {}}]}], [{explanation: 'done'}]])
104+
aidaClient: mockAidaClient([
105+
[{explanation: '', functionCalls: [{name: 'getNetworkActivitySummary', args: {}}]}], [{explanation: 'done'}]
106+
])
106107
});
107108
const activeInsight = new TimelineUtils.InsightAIContext.ActiveInsight(lcpPhases, parsedTrace);
108109
const context = new InsightContext(activeInsight);
@@ -124,8 +125,43 @@ What is this?`;
124125
return match;
125126
});
126127

127-
const expectedRequestsOutput = requests.map(r => TraceEventFormatter.networkRequest(r, parsedTrace));
128+
const expectedRequestsOutput =
129+
requests.map(r => TraceEventFormatter.networkRequest(r, parsedTrace, {verbose: false}));
128130
const expectedOutput = JSON.stringify({requests: expectedRequestsOutput});
131+
const titleResponse = responses.find(response => response.type === ResponseType.TITLE);
132+
assert.exists(titleResponse);
133+
assert.strictEqual(titleResponse.title, 'Investigating network activity…');
134+
135+
assert.exists(action);
136+
assert.deepEqual(
137+
action, {type: 'action' as ActionResponse['type'], output: expectedOutput, code: undefined, canceled: false});
138+
});
139+
140+
it('can call getNetworkRequestDetail to get detail about a single request', async function() {
141+
const {parsedTrace, insights} = await TraceLoader.traceEngine(this, 'lcp-images.json.gz');
142+
assert.isOk(insights);
143+
const [firstNav] = parsedTrace.Meta.mainFrameNavigations;
144+
const lcpPhases = getInsightOrError('LCPPhases', insights, firstNav);
145+
const requestUrl = 'https://chromedevtools.github.io/performance-stories/lcp-large-image/app.css';
146+
const agent = new PerformanceInsightsAgent({
147+
aidaClient: mockAidaClient([
148+
[{explanation: '', functionCalls: [{name: 'getNetworkRequestDetail', args: {url: requestUrl}}]}],
149+
[{explanation: 'done'}]
150+
])
151+
});
152+
const activeInsight = new TimelineUtils.InsightAIContext.ActiveInsight(lcpPhases, parsedTrace);
153+
const context = new InsightContext(activeInsight);
154+
155+
const responses = await Array.fromAsync(agent.run('test', {selected: context}));
156+
const titleResponse = responses.find(response => response.type === ResponseType.TITLE);
157+
assert.exists(titleResponse);
158+
assert.strictEqual(titleResponse.title, `Investigating network request ${requestUrl}…`);
159+
const action = responses.find(response => response.type === ResponseType.ACTION);
160+
const request = parsedTrace.NetworkRequests.byTime.find(r => r.args.data.url === requestUrl);
161+
assert.isOk(request);
162+
163+
const expectedRequestOutput = TraceEventFormatter.networkRequest(request, parsedTrace, {verbose: true});
164+
const expectedOutput = JSON.stringify({request: expectedRequestOutput});
129165

130166
assert.exists(action);
131167
assert.deepEqual(
@@ -145,14 +181,17 @@ What is this?`;
145181
const context = new InsightContext(activeInsight);
146182

147183
const responses = await Array.fromAsync(agent.run('test', {selected: context}));
184+
const titleResponse = responses.find(response => response.type === ResponseType.TITLE);
185+
assert.exists(titleResponse);
186+
assert.strictEqual(titleResponse.title, 'Investigating main thread activity…');
187+
148188
const action = responses.find(response => response.type === ResponseType.ACTION);
189+
assert.exists(action);
149190

150191
const expectedTree = TimelineUtils.InsightAIContext.AIQueries.mainThreadActivity(lcpPhases, parsedTrace);
151192
assert.isOk(expectedTree);
152-
153193
const expectedOutput = JSON.stringify({activity: expectedTree.serialize()});
154194

155-
assert.exists(action);
156195
assert.deepEqual(
157196
action, {type: 'action' as ActionResponse['type'], output: expectedOutput, code: undefined, canceled: false});
158197
});

front_end/panels/ai_assistance/agents/PerformanceInsightsAgent.ts

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type * as Lit from '../../../ui/lit/lit.js';
88
import * as TimelineUtils from '../../timeline/utils/utils.js';
99
import * as PanelUtils from '../../utils/utils.js';
1010
import {PerformanceInsightFormatter, TraceEventFormatter} from '../data_formatters/PerformanceInsightFormatter.js';
11+
import {debugLog} from '../debug.js';
1112

1213
import {
1314
type AgentOptions as BaseAgentOptions,
@@ -25,7 +26,7 @@ const UIStringsNotTranslated = {
2526
/**
2627
*@description Shown when the agent is investigating network activity
2728
*/
28-
networkActivity: 'Investigating network activity…',
29+
networkActivitySummary: 'Investigating network activity…',
2930
/**
3031
*@description Shown when the agent is investigating main thread activity
3132
*/
@@ -48,12 +49,14 @@ You will also be provided with external resources. Use these to ensure you give
4849
4950
- Think about what the user wants.
5051
- Call any of the available functions to help you gather more information to inform your suggestions.
52+
- Ensure that you call all relevant functions to receive full information about relevant network requests.
5153
- Make suggestions that you are confident will improve the performance of the page.
5254
5355
## General considerations
5456
5557
- *CRITICAL* never make the same function call twice.
5658
- *CRITICAL* make sure you are thorough and call the functions you have access to to give yourself the most information possible to make accurate recommendations.
59+
- *CRITICAL* your text output should NEVER mention the functions that you called. These are an implementation detail and not important for the user to be aware of.
5760
`;
5861
/* clang-format on */
5962

@@ -134,18 +137,20 @@ export class PerformanceInsightsAgent extends AiAgent<TimelineUtils.InsightAICon
134137

135138
this.declareFunction<Record<never, unknown>, {
136139
requests: string[],
137-
}>('getNetworkActivity', {
138-
description: 'Returns relevant network requests for the selected insight',
140+
}>('getNetworkActivitySummary', {
141+
description:
142+
'Returns a summary of network activity for the selected insight. If you want to get more detailed information on a network request, you can pass the URL of a request into `getNetworkRequestDetail`.',
139143
parameters: {
140144
type: Host.AidaClient.ParametersTypes.OBJECT,
141145
description: '',
142146
nullable: true,
143147
properties: {},
144148
},
145149
displayInfoFromArgs: () => {
146-
return {title: lockedString(UIStringsNotTranslated.networkActivity)};
150+
return {title: lockedString(UIStringsNotTranslated.networkActivitySummary)};
147151
},
148152
handler: async () => {
153+
debugLog('Function call: getNetworkActivitySummary');
149154
if (!this.#insight) {
150155
return {error: 'No insight available'};
151156
}
@@ -154,11 +159,46 @@ export class PerformanceInsightsAgent extends AiAgent<TimelineUtils.InsightAICon
154159
activeInsight.insight,
155160
activeInsight.parsedTrace,
156161
);
157-
const formatted = requests.map(r => TraceEventFormatter.networkRequest(r, activeInsight.parsedTrace));
162+
const formatted =
163+
requests.map(r => TraceEventFormatter.networkRequest(r, activeInsight.parsedTrace, {verbose: false}));
158164
return {result: {requests: formatted}};
159165
},
160166
});
161167

168+
this.declareFunction<Record<'url', string>, {
169+
request: string,
170+
}>('getNetworkRequestDetail', {
171+
description: 'Returns detailed debugging information about a specific network request',
172+
parameters: {
173+
type: Host.AidaClient.ParametersTypes.OBJECT,
174+
description: '',
175+
nullable: true,
176+
properties: {
177+
url: {
178+
type: Host.AidaClient.ParametersTypes.STRING,
179+
description: 'The URL of the network request',
180+
nullable: false,
181+
}
182+
},
183+
},
184+
displayInfoFromArgs: params => {
185+
return {title: lockedString(`Investigating network request ${params.url}…`)};
186+
},
187+
handler: async params => {
188+
debugLog('Function call: getNetworkRequestDetail', params);
189+
if (!this.#insight) {
190+
return {error: 'No insight available'};
191+
}
192+
const activeInsight = this.#insight.getItem();
193+
const request = TimelineUtils.InsightAIContext.AIQueries.networkRequest(activeInsight.parsedTrace, params.url);
194+
if (!request) {
195+
return {error: 'Request not found'};
196+
}
197+
const formatted = TraceEventFormatter.networkRequest(request, activeInsight.parsedTrace, {verbose: true});
198+
return {result: {request: formatted}};
199+
},
200+
});
201+
162202
this.declareFunction<Record<never, unknown>, {activity: string}>('getMainThreadActivity', {
163203
description: `Returns the main thread activity for the selected insight.
164204
The tree is represented as a call frame with a root task and a series of children.
@@ -191,6 +231,7 @@ The fields are:
191231
return {title: lockedString(UIStringsNotTranslated.mainThreadActivity)};
192232
},
193233
handler: async () => {
234+
debugLog('Function call: getMainThreadActivity');
194235
if (!this.#insight) {
195236
return {error: 'No insight available'};
196237
}

front_end/panels/ai_assistance/data_formatters/PerformanceInsightFormatter.test.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,20 +73,22 @@ We can break this time down into the 2 phases that combine to make up the LCP ti
7373
});
7474

7575
describe('Formatting TraceEvents', () => {
76-
it('formats network requests', async function() {
76+
it('formats network requests in verbose mode', async function() {
7777
const {parsedTrace} = await TraceLoader.traceEngine(this, 'lcp-images.json.gz');
7878
const requestUrl = 'https://fonts.googleapis.com/css2?family=Poppins:ital,wght@1,800';
7979
const request = parsedTrace.NetworkRequests.byTime.find(r => r.args.data.url === requestUrl);
8080
assert.isOk(request);
81-
const output = TraceEventFormatter.networkRequest(request, parsedTrace);
81+
const output = TraceEventFormatter.networkRequest(request, parsedTrace, {verbose: true});
8282
const expected = `## Network request: https://fonts.googleapis.com/css2?family=Poppins:ital,wght@1,800
8383
Timings:
8484
- Start time: 37.62 ms
8585
- Queued at: 43.24 ms
8686
- Request sent at: 41.71 ms
8787
- Download complete at: 48.04 ms
88-
- Fully completed at: 51.55 ms
89-
- Total request duration: 13.93 ms
88+
- Completed at: 51.55 ms
89+
Durations:
90+
- Main thread processing duration: 3.51 ms
91+
- Total duration: 13.93 ms
9092
Status code: 200
9193
MIME Type: text/css
9294
Priority:
@@ -111,6 +113,20 @@ Response headers
111113
- x-xss-protection: 0
112114
- expires: Thu, 07 Mar 2024 21:17:02 GMT`;
113115

116+
assert.strictEqual(output, expected);
117+
});
118+
it('formats network requests in non-verbose mode', async function() {
119+
const {parsedTrace} = await TraceLoader.traceEngine(this, 'lcp-images.json.gz');
120+
const requestUrl = 'https://fonts.googleapis.com/css2?family=Poppins:ital,wght@1,800';
121+
const request = parsedTrace.NetworkRequests.byTime.find(r => r.args.data.url === requestUrl);
122+
assert.isOk(request);
123+
const output = TraceEventFormatter.networkRequest(request, parsedTrace, {verbose: false});
124+
const expected = `## Network request: https://fonts.googleapis.com/css2?family=Poppins:ital,wght@1,800
125+
- Start time: 37.62 ms
126+
- Duration: 13.93 ms
127+
- MIME type: text/css
128+
- This request was render blocking`;
129+
114130
assert.strictEqual(output, expected);
115131
});
116132
});

front_end/panels/ai_assistance/data_formatters/PerformanceInsightFormatter.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ ${phaseBulletPoints.map(phase => `- ${phase.name}: ${phase.value}`).join('\n')}`
155155
}
156156
}
157157

158+
export interface NetworkRequestFormatOptions {
159+
verbose: boolean;
160+
}
161+
158162
export class TraceEventFormatter {
159163
/**
160164
* This is the data passed to a network request when the Performance Insights
@@ -165,7 +169,8 @@ export class TraceEventFormatter {
165169
* talk to jacktfranklin@.
166170
*/
167171
static networkRequest(
168-
request: Trace.Types.Events.SyntheticNetworkRequest, parsedTrace: Trace.Handlers.Types.ParsedTrace): string {
172+
request: Trace.Types.Events.SyntheticNetworkRequest, parsedTrace: Trace.Handlers.Types.ParsedTrace,
173+
options: NetworkRequestFormatOptions): string {
169174
const {url, statusCode, initialPriority, priority, fromServiceWorker, mimeType, responseHeaders, syntheticData} =
170175
request.args.data;
171176

@@ -187,19 +192,30 @@ export class TraceEventFormatter {
187192
requestSent: syntheticData.sendStartTime - baseTime,
188193
downloadComplete: syntheticData.finishTime - baseTime,
189194
processingComplete: request.ts + request.dur - baseTime,
190-
191195
} as const;
192196

197+
const mainThreadProcessingDuration =
198+
startTimesForLifecycle.processingComplete - startTimesForLifecycle.downloadComplete;
199+
193200
const renderBlocking = Trace.Helpers.Network.isSyntheticNetworkRequestEventRenderBlocking(request);
194201

202+
if (!options.verbose) {
203+
return `## Network request: ${url}
204+
- Start time: ${formatMicro(startTimesForLifecycle.start)}
205+
- Duration: ${formatMicro(request.dur)}
206+
- MIME type: ${mimeType}${renderBlocking ? '\n- This request was render blocking' : ''}`;
207+
}
208+
195209
return `## Network request: ${url}
196210
Timings:
197211
- Start time: ${formatMicro(startTimesForLifecycle.start)}
198212
- Queued at: ${formatMicro(startTimesForLifecycle.queueing)}
199213
- Request sent at: ${formatMicro(startTimesForLifecycle.requestSent)}
200214
- Download complete at: ${formatMicro(startTimesForLifecycle.downloadComplete)}
201-
- Fully completed at: ${formatMicro(startTimesForLifecycle.processingComplete)}
202-
- Total request duration: ${formatMicro(request.dur)}
215+
- Completed at: ${formatMicro(startTimesForLifecycle.processingComplete)}
216+
Durations:
217+
- Main thread processing duration: ${formatMicro(mainThreadProcessingDuration)}
218+
- Total duration: ${formatMicro(request.dur)}
203219
Status code: ${statusCode}
204220
MIME Type: ${mimeType}
205221
Priority:

front_end/panels/timeline/utils/InsightAIContext.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,18 @@ export class AIQueries {
6464
return matchedRequests;
6565
}
6666

67+
/**
68+
* Returns the single network request. We do not check to filter this by the
69+
* bounds of the insight, because the only way that the LLM has found this
70+
* request is by first inspecting a summary of relevant network requests for
71+
* the given insight. So if it then looks up a request by URL, we know that
72+
* is a valid and relevant request.
73+
*/
74+
static networkRequest(parsedTrace: Trace.Handlers.Types.ParsedTrace, url: string):
75+
Trace.Types.Events.SyntheticNetworkRequest|null {
76+
return parsedTrace.NetworkRequests.byTime.find(r => r.args.data.url === url) ?? null;
77+
}
78+
6779
/**
6880
* Returns an AI Call Tree representing the activity on the main thread for
6981
* the relevant time range of the given insight.

0 commit comments

Comments
 (0)