Skip to content

Commit f696aba

Browse files
paulirishDevtools-frontend LUCI CQ
authored andcommitted
RPP: Introduce AICallTree, rewrite of TreeHelper AINode
Revamp of the data structure and serialization format of a flame chart call tree, being passed to Freestyler. This impl leverages TimelineProfileTree for faster/easier tree construction and manipulation. We build our tree with only the events from the same track that overlap the selected event. Serialization: We switch from JSON to a YAML-like format that's 35% more efficient with the tokenizer. A few variants were tested and this format was chosen to be token-efficient and meaningfully clear to the LLM. Notably: - rather than nested children, we use adjacency lists. - URLs are particularly token-inefficient. To avoid repeating, we output a lookup table. Within the Freestyler chat UI, the Call frame chip is now clickable. It relies on a new Revealable class: SDK.TraceObject.RevealableEvent Bug: 370436840,372658699 Change-Id: I89baabfd9e7b1fdd183a6a9ea12bf2a318125fbc Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/5963059 Reviewed-by: Jack Franklin <[email protected]> Commit-Queue: Jack Franklin <[email protected]> Auto-Submit: Paul Irish <[email protected]>
1 parent b246774 commit f696aba

23 files changed

+474
-159
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1865,6 +1865,7 @@ grd_files_debug_sources = [
18651865
"front_end/panels/timeline/timelinePaintProfiler.css.js",
18661866
"front_end/panels/timeline/timelinePanel.css.js",
18671867
"front_end/panels/timeline/timelineStatusDialog.css.js",
1868+
"front_end/panels/timeline/utils/AICallTree.js",
18681869
"front_end/panels/timeline/utils/EntryName.js",
18691870
"front_end/panels/timeline/utils/EntryStyles.js",
18701871
"front_end/panels/timeline/utils/Helpers.js",

front_end/core/sdk/TraceObject.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,11 @@ export class TraceObject {
1313
this.metadata = metadata;
1414
}
1515
}
16+
17+
// Another thin wrapper class to enable revealing individual trace events (aka entries) in Timeline panel.
18+
export class RevealableEvent {
19+
// Only Trace.Types.Events.Event are passed in, but we can't depend on that type from SDK
20+
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
21+
constructor(public event: any) {
22+
}
23+
}

front_end/models/timeline_model/TimelineModelFilter.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ export class TimelineVisibleEventsFilter extends TimelineModelFilter {
2424

2525
static eventType(event: Trace.Types.Events.Event): Trace.Types.Events.Name {
2626
// Any blink.console category events are treated as ConsoleTime events
27-
if (Trace.Helpers.Trace.eventHasCategory(event, 'blink.console')) {
27+
if (event.cat.includes('blink.console')) {
2828
return Trace.Types.Events.Name.CONSOLE_TIME;
2929
}
3030
// Any blink.user_timing egory events are treated as UserTiming events
31-
if (Trace.Helpers.Trace.eventHasCategory(event, 'blink.user_timing')) {
31+
if (event.cat.includes('blink.user_timing')) {
3232
return Trace.Types.Events.Name.USER_TIMING;
3333
}
3434
return event.name as Trace.Types.Events.Name;

front_end/panels/freestyler/BUILD.gn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ devtools_module("freestyler") {
4242
"../../models/trace:bundle",
4343
"../../models/workspace:bundle",
4444
"../../panels/network:bundle",
45+
"../../panels/timeline/utils:bundle",
4546
"../../panels/utils:bundle",
4647
"../../third_party/marked:bundle",
4748
"../../ui/components/markdown_view:bundle",
@@ -99,6 +100,7 @@ ts_library("unittests") {
99100
"../../generated:protocol",
100101
"../../models/bindings:bundle",
101102
"../../models/trace:bundle",
103+
"../../panels/timeline/utils:bundle",
102104
"../../testing",
103105
]
104106
}

front_end/panels/freestyler/DrJonesPerformanceAgent.test.ts

Lines changed: 32 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
// found in the LICENSE file.
44

55
import type * as Host from '../../core/host/host.js';
6-
import * as Trace from '../../models/trace/trace.js';
76
import {describeWithEnvironment, getGetHostConfigStub} from '../../testing/EnvironmentHelpers.js';
8-
import {makeCompleteEvent, makeProfileCall} from '../../testing/TraceHelpers.js';
7+
import {TraceLoader} from '../../testing/TraceLoader.js';
8+
import * as TimelineUtils from '../timeline/utils/utils.js';
99

1010
import {DrJonesPerformanceAgent, ResponseType} from './freestyler.js';
1111

@@ -110,30 +110,15 @@ describeWithEnvironment('DrJonesPerformanceAgent', () => {
110110
});
111111
});
112112
describe('run', function() {
113-
const evaluateScript = makeCompleteEvent(Trace.Types.Events.Name.EVALUATE_SCRIPT, 0, 500);
114-
const v8Run = makeCompleteEvent('v8.run', 10, 490);
115-
const parseFunction = makeCompleteEvent('V8.ParseFunction', 12, 1);
116-
const traceEvents: Trace.Types.Events.Event[] = [evaluateScript, v8Run, parseFunction];
117-
const profileCalls = [makeProfileCall('a', 100, 200), makeProfileCall('b', 300, 200)];
118-
119-
// Roughly this looks like:
120-
// 0 500
121-
// |------------- EvaluateScript -------------|
122-
// |- v8.run -|
123-
// |--| |- a -||- b |
124-
// ^ V8.ParseFunction
125-
126-
const allEntries = Trace.Helpers.Trace.mergeEventsInOrder(traceEvents, profileCalls);
127-
const {entryToNode} = Trace.Helpers.TreeHelpers.treify(allEntries, {filter: {has: () => true}});
128-
const selectedNode = entryToNode.get(v8Run);
129-
assert.exists(selectedNode);
130-
131-
const aiNodeTree = Trace.Helpers.TreeHelpers.AINode.fromEntryNode(selectedNode, () => true);
132-
const v8RunNode = Trace.Helpers.TreeHelpers.AINode.getSelectedNodeWithinTree(aiNodeTree);
133-
assert.exists(aiNodeTree);
134-
assert.exists(v8RunNode);
135-
136-
it('generates an answer', async () => {
113+
it('generates an answer', async function() {
114+
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev-outermost-frames.json.gz');
115+
// A basic Layout.
116+
const layoutEvt = parsedTrace.Renderer.allTraceEntries.find(event => event.ts === 465457096322);
117+
assert.exists(layoutEvt);
118+
const aiCallTree =
119+
TimelineUtils.AICallTree.AICallTree.from(layoutEvt, parsedTrace.Renderer.allTraceEntries, parsedTrace);
120+
assert.exists(aiCallTree);
121+
137122
async function* generateAnswer() {
138123
yield {
139124
explanation: 'This is the answer',
@@ -148,9 +133,23 @@ describeWithEnvironment('DrJonesPerformanceAgent', () => {
148133
aidaClient: mockAidaClient(generateAnswer),
149134
});
150135

151-
// Select the v8.run node
152-
v8RunNode.selected = true;
153-
const responses = await Array.fromAsync(agent.run('test', {selected: aiNodeTree}));
136+
const responses = await Array.fromAsync(agent.run('test', {selected: aiCallTree}));
137+
const expectedData = '\n\n' +
138+
`
139+
140+
141+
# Call tree:
142+
143+
Node: 1 – Task
144+
dur: 3
145+
Children:
146+
* 2 – Layout
147+
148+
Node: 2 – Layout
149+
Selected: true
150+
dur: 3
151+
self: 3
152+
`.trim();
154153

155154
assert.deepStrictEqual(responses, [
156155
{
@@ -159,33 +158,14 @@ describeWithEnvironment('DrJonesPerformanceAgent', () => {
159158
},
160159
{
161160
type: ResponseType.CONTEXT,
162-
title: 'Analyzing stack',
161+
title: 'Analyzing call tree',
163162
details: [
164-
{
165-
title: 'Selected stack',
166-
text: JSON.stringify({
167-
name: 'EvaluateScript',
168-
dur: 0.5,
169-
self: 0,
170-
children: [{
171-
selected: true,
172-
name: 'v8.run',
173-
dur: 0.5,
174-
self: 0.1,
175-
children: [
176-
{name: 'V8.ParseFunction', dur: 0, self: 0},
177-
{name: 'a', url: '', dur: 0.2, self: 0.2},
178-
{name: 'b', url: '', dur: 0.2, self: 0.2},
179-
],
180-
}],
181-
}),
182-
},
163+
{title: 'Selected call tree', text: expectedData},
183164
],
184165
},
185166
{
186167
type: ResponseType.QUERYING,
187-
query:
188-
'# Selected stack trace\n{\"name\":\"EvaluateScript\",\"dur\":0.5,\"self\":0}\n\n# User request\n\ntest',
168+
query: `\n${expectedData}\n\n# User request\n\ntest`,
189169
},
190170
{
191171
type: ResponseType.ANSWER,
@@ -198,7 +178,7 @@ describeWithEnvironment('DrJonesPerformanceAgent', () => {
198178
assert.deepStrictEqual(agent.chatHistoryForTesting, [
199179
{
200180
entity: 1,
201-
text: `# Selected stack trace\n${JSON.stringify(aiNodeTree)}\n\n# User request\n\ntest`,
181+
text: `\n${aiCallTree.serialize()}\n\n# User request\n\ntest`,
202182
},
203183
{
204184
entity: 2,

front_end/panels/freestyler/DrJonesPerformanceAgent.ts

Lines changed: 85 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import * as Common from '../../core/common/common.js';
66
import * as Host from '../../core/host/host.js';
77
import * as i18n from '../../core/i18n/i18n.js';
8-
import type * as Trace from '../../models/trace/trace.js';
8+
import type * as TimelineUtils from '../../panels/timeline/utils/utils.js';
99

1010
import {
1111
AiAgent,
@@ -15,39 +15,91 @@ import {
1515
ResponseType,
1616
} from './AiAgent.js';
1717

18+
/**
19+
* Preamble clocks in at ~950 tokens.
20+
* The prose is around 4.5 chars per token.
21+
* The data can be as bad as 1.8 chars per token
22+
*
23+
* Check token length in https://aistudio.google.com/
24+
*/
1825
const preamble = `You are a performance expert deeply integrated with Chrome DevTools.
19-
You specialize in analyzing web application behavior captured by Chrome DevTools Performance Panel.
20-
You will be provided with a string containing the JSON.stringify representation of a tree of events captured in a Chrome DevTools performance recording.
21-
This tree originates from the root task of a specific event that was selected by a user in the Performance panel's flame chart.
22-
Each node in this tree represents an event and contains the following information:
23-
24-
* name: The name of the event or JavaScript function
25-
* url: The URL of the JavaScript file where the event originated. If present, this event is a JavaScript function. If not, it's a native browser task.
26-
* dur: The total duration of the event, including the time spent in its children, in milliseconds.
27-
* self: The duration of the event itself, excluding the time spent in its children, in milliseconds.
28-
* selected: A boolean value indicating whether this is the event the user selected in the Performance panel.
29-
* children: An array of child events, each represented as another node with the same structure.
30-
31-
Your task is to analyze this event and its surrounding context within the performance recording. Your analysis may include:
32-
* Clearly state the name and purpose of the selected event based on its properties (e.g., function name, URL, line number). Explain what the task is broadly doing. You can also mention the function
26+
You specialize in analyzing web application behavior captured by Chrome DevTools Performance Panel and Chrome tracing.
27+
You will be provided a text representation of a call tree of native and JavaScript callframes selected by the user from a performance trace's flame chart.
28+
This tree originates from the root task of a specific callframe.
29+
30+
The format of each callframe is:
31+
32+
Node: $id – $name
33+
Selected: true
34+
dur: $duration
35+
self: $self
36+
URL #: $url_number
37+
Children:
38+
* $child.id – $child.name
39+
40+
The fields are:
41+
42+
* name: A short string naming the callframe (e.g. 'Evaluate Script' or the JS function name 'InitializeApp')
43+
* id: A numerical identifier for the callframe
44+
* Selected: Set to true if this callframe is the one the user selected.
45+
* url_number: The number of the URL referenced in the "All URLs" list
46+
* dur: The total duration of the callframe (includes time spent in its descendants), in milliseconds.
47+
* self: The self duration of the callframe (excludes time spent in its descendants), in milliseconds. If omitted, assume the value is 0.
48+
* children: An list of child callframes, each denoted by their id and name
49+
50+
Your task is to analyze this callframe and its surrounding context within the performance recording. Your analysis may include:
51+
* Clearly state the name and purpose of the selected callframe based on its properties (e.g., name, URL). Explain what the task is broadly doing.
3352
* Describe its execution context:
34-
* Ancestors: Trace back through the tree to identify the chain of parent events that led to the execution of the selected event. Describe this execution path.
35-
* Descendants: Analyze the children of the selected event. What tasks did it initiate? Did it spawn any long-running or resource-intensive sub-tasks?
53+
* Ancestors: Trace back through the tree to identify the chain of parent callframes that led to the execution of the selected callframe. Describe this execution path.
54+
* Descendants: Analyze the children of the selected callframe. What tasks did it initiate? Did it spawn any long-running or resource-intensive sub-tasks?
3655
* Quantify performance:
3756
* Duration
38-
* Relative Cost: How much did this event contribute to the overall duration of its parent tasks and the entire recorded trace?
39-
* Potential Bottlenecks: Analyze the totalTime and selfTime of the selected event and its children to identify any potential performance bottlenecks. Are there any excessively long tasks or periods of idle time?
40-
4. (Only provide if you have specific suggestions) Based on your analysis, provide specific and actionable suggestions for improving the performance of the selected event and its related tasks. Are there any resources being acquired or held for longer than necessary?
57+
* Relative Cost: How much did this callframe contribute to the overall duration of its parent tasks and the entire recorded trace?
58+
* Potential Bottlenecks: Analyze the total and self duration of the selected callframe and its children to identify any potential performance bottlenecks. Are there any excessively long tasks or periods of idle time?
59+
4. Based on your analysis, provide specific and actionable suggestions for improving the performance of the selected callframe and its related tasks. Are there any resources being acquired or held for longer than necessary? Only provide if you have specific suggestions.
4160
4261
# Considerations
4362
* Keep your analysis concise and focused, highlighting only the most critical aspects for a software engineer.
44-
* Do not mention id of the event in your response.
63+
* Do not mention id of the callframe or the URL number in your response.
4564
* **CRITICAL** If the user asks a question about religion, race, politics, sexuality, gender, or other sensitive topics, answer with "Sorry, I can't answer that. I'm best at questions about performance of websites."
4665
4766
## Example session
4867
49-
Selected call tree
50-
'{"id":"1","function":"main","start":0,"totalTime":500,"selfTime":100,"children":[{"id":"2","function":"update","start":100,"totalTime":200,"selfTime":50,"children":[{"id":"3","function":"animate","url":"[invalid URL removed]","line":120,"column":10,"start":150,"totalTime":150,"selfTime":20,"selected":true,"children":[{"id":"4","function":"calculatePosition","start":160,"totalTime":80,"selfTime":80,"children":[]},{"id":"5","function":"applyStyles","start":240,"totalTime":50,"selfTime":50,"children":[]}]}]}]}'
68+
All URL #s:
69+
70+
* 0 – app.js
71+
72+
Call tree:
73+
74+
Node: 1 – main
75+
dur: 500
76+
self: 100
77+
Children:
78+
* 2 – update
79+
80+
Node: 2 – update
81+
dur: 200
82+
self: 50
83+
Children:
84+
* 3 – animate
85+
86+
Node: 3 – animate
87+
Selected: true
88+
dur: 150
89+
self: 20
90+
URL #: 0
91+
Children:
92+
* 4 – calculatePosition
93+
* 5 – applyStyles
94+
95+
Node: 4 – calculatePosition
96+
dur: 80
97+
self: 80
98+
99+
Node: 5 – applyStyles
100+
dur: 50
101+
self: 50
102+
51103
Explain the selected task.
52104
53105
@@ -62,7 +114,7 @@ Perhaps there's room for optimization there. You could investigate whether the c
62114
* Strings that don't need to be translated at this time.
63115
*/
64116
const UIStringsNotTranslate = {
65-
analyzingStackTrace: 'Analyzing stack',
117+
analyzingCallTree: 'Analyzing call tree',
66118
};
67119

68120
const lockedString = i18n.i18n.lockedString;
@@ -71,7 +123,7 @@ const lockedString = i18n.i18n.lockedString;
71123
* One agent instance handles one conversation. Create a new agent
72124
* instance for a new conversation.
73125
*/
74-
export class DrJonesPerformanceAgent extends AiAgent<Trace.Helpers.TreeHelpers.AINode> {
126+
export class DrJonesPerformanceAgent extends AiAgent<TimelineUtils.AICallTree.AICallTree> {
75127
readonly preamble = preamble;
76128
readonly clientFeature = Host.AidaClient.ClientFeature.CHROME_DRJONES_PERFORMANCE_AGENT;
77129
get userTier(): string|undefined {
@@ -90,25 +142,24 @@ export class DrJonesPerformanceAgent extends AiAgent<Trace.Helpers.TreeHelpers.A
90142
}
91143

92144
async *
93-
handleContextDetails(selectedStackTrace: Trace.Helpers.TreeHelpers.AINode|null):
145+
handleContextDetails(aiCallTree: TimelineUtils.AICallTree.AICallTree|null):
94146
AsyncGenerator<ContextResponse, void, void> {
95147
yield {
96148
type: ResponseType.CONTEXT,
97-
title: lockedString(UIStringsNotTranslate.analyzingStackTrace),
149+
title: lockedString(UIStringsNotTranslate.analyzingCallTree),
98150
details: [
99151
{
100-
title: 'Selected stack',
101-
text: JSON.stringify(selectedStackTrace),
152+
title: 'Selected call tree',
153+
text: aiCallTree?.serialize() ?? '',
102154
},
103155
],
104156
};
105157
}
106158

107-
override async enhanceQuery(query: string, selectedStackTrace: Trace.Helpers.TreeHelpers.AINode|null):
108-
Promise<string> {
109-
selectedStackTrace?.sanitize();
110-
const stackStr = JSON.stringify(selectedStackTrace).trim();
111-
const perfEnhancementQuery = selectedStackTrace ? `# Selected stack trace\n${stackStr}\n\n# User request\n\n` : '';
159+
override async enhanceQuery(query: string, aiCallTree: TimelineUtils.AICallTree.AICallTree|null): Promise<string> {
160+
const treeStr = aiCallTree?.serialize();
161+
162+
const perfEnhancementQuery = aiCallTree ? `\n${treeStr}\n\n# User request\n\n` : '';
112163
return `${perfEnhancementQuery}${query}`;
113164
}
114165

0 commit comments

Comments
 (0)