Skip to content

Commit 1317f2e

Browse files
jackfranklinDevtools-frontend LUCI CQ
authored andcommitted
Allow AICallTree to be created from a time range
This is to enable us to pass the LLM data from the main thread for a given range, rather than for the icicle of a specific, selected event. Bug: 394552594 Change-Id: Icf66b06a3fde0649c9fb6bd80f868f9fa7ee1019 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6269154 Commit-Queue: Jack Franklin <[email protected]> Reviewed-by: Paul Irish <[email protected]>
1 parent 4ca1a2c commit 1317f2e

File tree

8 files changed

+163
-49
lines changed

8 files changed

+163
-49
lines changed

front_end/panels/ai_assistance/AiAssistancePanel.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -714,7 +714,9 @@ export class AiAssistancePanel extends UI.Panel.Panel {
714714
return Common.Revealer.reveal(context.getItem().uiLocation(0, 0));
715715
}
716716
if (context instanceof CallTreeContext) {
717-
const trace = new SDK.TraceObject.RevealableEvent(context.getItem().selectedNode.event);
717+
const item = context.getItem();
718+
const event = item.selectedNode?.event ?? item.rootNode.event;
719+
const trace = new SDK.TraceObject.RevealableEvent(event);
718720
return Common.Revealer.reveal(trace);
719721
}
720722
// Node picker is using linkifier.

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ describeWithEnvironment('PerformanceAgent', () => {
2727
const evalScriptEvent = parsedTrace.Renderer.allTraceEntries.find(
2828
event => event.name === Trace.Types.Events.Name.EVALUATE_SCRIPT && event.ts === 122411195649);
2929
assert.exists(evalScriptEvent);
30-
const aiCallTree = TimelineUtils.AICallTree.AICallTree.from(evalScriptEvent, parsedTrace);
30+
const aiCallTree = TimelineUtils.AICallTree.AICallTree.fromEvent(evalScriptEvent, parsedTrace);
3131
assert.isOk(aiCallTree);
3232
const callTreeContext = new CallTreeContext(aiCallTree);
3333
assert.strictEqual(callTreeContext.getOrigin(), 'https://www.googletagmanager.com');
@@ -39,7 +39,7 @@ describeWithEnvironment('PerformanceAgent', () => {
3939
const layoutEvent = parsedTrace.Renderer.allTraceEntries.find(
4040
event => event.name === Trace.Types.Events.Name.LAYOUT && event.ts === 122411130078);
4141
assert.exists(layoutEvent);
42-
const aiCallTree = TimelineUtils.AICallTree.AICallTree.from(layoutEvent, parsedTrace);
42+
const aiCallTree = TimelineUtils.AICallTree.AICallTree.fromEvent(layoutEvent, parsedTrace);
4343
assert.isOk(aiCallTree);
4444
const callTreeContext = new CallTreeContext(aiCallTree);
4545
assert.strictEqual(callTreeContext.getOrigin(), 'Layout_90829_259_122411130078');
@@ -125,7 +125,7 @@ describeWithEnvironment('PerformanceAgent', () => {
125125
// A basic Layout.
126126
const layoutEvt = parsedTrace.Renderer.allTraceEntries.find(event => event.ts === 465457096322);
127127
assert.exists(layoutEvt);
128-
const aiCallTree = TimelineUtils.AICallTree.AICallTree.from(layoutEvt, parsedTrace);
128+
const aiCallTree = TimelineUtils.AICallTree.AICallTree.fromEvent(layoutEvt, parsedTrace);
129129
assert.exists(aiCallTree);
130130

131131
const agent = new PerformanceAgent({

front_end/panels/ai_assistance/agents/PerformanceAgent.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,12 @@ export class CallTreeContext extends ConversationContext<TimelineUtils.AICallTre
132132
}
133133

134134
override getOrigin(): string {
135-
const selectedEvent = this.#callTree.selectedNode.event;
135+
// Although in this context we expect the call tree to have a selected node
136+
// as the entrypoint into the "Ask AI" tool is via selecting a node, it is
137+
// possible to build trees without a selected node, in which case we
138+
// fallback to the root node.
139+
const node = this.#callTree.selectedNode ?? this.#callTree.rootNode;
140+
const selectedEvent = node.event;
136141
// Get the non-resolved (ignore sourcemaps) URL for the event. We use the
137142
// non-resolved URL as in the context of the AI Assistance panel, we care
138143
// about the origin it was served on.
@@ -168,7 +173,7 @@ export class CallTreeContext extends ConversationContext<TimelineUtils.AICallTre
168173
}
169174

170175
override getTitle(): string {
171-
const {event} = this.#callTree.selectedNode;
176+
const event = this.#callTree.selectedNode?.event ?? this.#callTree.rootNode.event;
172177
if (!event) {
173178
return 'unknown';
174179
}

front_end/panels/timeline/TimelineFlameChartDataProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ export class TimelineFlameChartDataProvider extends Common.ObjectWrapper.ObjectW
229229

230230
const contextMenu = new UI.ContextMenu.ContextMenu(mouseEvent);
231231
if (perfAIEntryPointEnabled && this.parsedTrace) {
232-
const aiCallTree = Utils.AICallTree.AICallTree.from(entry, this.parsedTrace);
232+
const aiCallTree = Utils.AICallTree.AICallTree.fromEvent(entry, this.parsedTrace);
233233
if (aiCallTree) {
234234
const action = UI.ActionRegistry.ActionRegistry.instance().getAction(PERF_AI_ACTION_ID);
235235
contextMenu.footerSection().appendItem(action.title(), () => {

front_end/panels/timeline/TimelineFlameChartView.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -790,7 +790,7 @@ describeWithEnvironment('TimelineFlameChartView', function() {
790790
// Find some task in the main thread that we can build an AI Call Tree from
791791
const task = parsedTrace.Renderer.allTraceEntries.find(event => {
792792
return Trace.Types.Events.isRunTask(event) && event.dur > 5_000 &&
793-
Utils.AICallTree.AICallTree.from(event, parsedTrace) !== null;
793+
Utils.AICallTree.AICallTree.fromEvent(event, parsedTrace) !== null;
794794
});
795795

796796
assert.isOk(task);

front_end/panels/timeline/TimelineFlameChartView.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1409,7 +1409,7 @@ export class TimelineFlameChartView extends Common.ObjectWrapper.eventMixin<Even
14091409
// an invalid event - we don't want to reset it back as it may be they are
14101410
// clicking around in order to understand something.
14111411
if (selectionIsEvent(selection) && this.#parsedTrace) {
1412-
const aiCallTree = Utils.AICallTree.AICallTree.from(selection.event, this.#parsedTrace);
1412+
const aiCallTree = Utils.AICallTree.AICallTree.fromEvent(selection.event, this.#parsedTrace);
14131413
if (aiCallTree) {
14141414
UI.Context.Context.instance().setFlavor(Utils.AICallTree.AICallTree, aiCallTree);
14151415
}

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

Lines changed: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ describeWithEnvironment('AICallTree', () => {
1717
return e.name === Trace.Types.Events.Name.RASTER_TASK && e.pid === 4274 && e.tid === 23555;
1818
});
1919
assert.isOk(rasterTask);
20-
assert.isNull(Utils.AICallTree.AICallTree.from(rasterTask, parsedTrace));
20+
assert.isNull(Utils.AICallTree.AICallTree.fromEvent(rasterTask, parsedTrace));
2121
});
2222

2323
it('does not build a tree from events the renderer is not aware of', async function() {
@@ -26,7 +26,7 @@ describeWithEnvironment('AICallTree', () => {
2626
const shift = parsedTrace.LayoutShifts.clusters.at(0)?.events.at(0);
2727
assert.isOk(shift);
2828
assert.isTrue(Trace.Types.Events.isSyntheticLayoutShift(shift));
29-
assert.isNull(Utils.AICallTree.AICallTree.from(shift, parsedTrace));
29+
assert.isNull(Utils.AICallTree.AICallTree.fromEvent(shift, parsedTrace));
3030
});
3131

3232
it('supports NodeJS traces that do not have a "main thread"', async function() {
@@ -44,7 +44,7 @@ describeWithEnvironment('AICallTree', () => {
4444
return Trace.Types.Events.isProfileCall(event) && event.callFrame.functionName === 'callAndPauseOnStart';
4545
});
4646
assert.isOk(funcCall);
47-
const callTree = Utils.AICallTree.AICallTree.from(funcCall, parsedTrace);
47+
const callTree = Utils.AICallTree.AICallTree.fromEvent(funcCall, parsedTrace);
4848
assert.isOk(callTree);
4949
const expectedData = '\n' +
5050
`
@@ -117,7 +117,7 @@ URL #: 3
117117
if (!selectedEvent) {
118118
throw new Error('Could not find expected event.');
119119
}
120-
const callTree = Utils.AICallTree.AICallTree.from(selectedEvent, parsedTrace);
120+
const callTree = Utils.AICallTree.AICallTree.fromEvent(selectedEvent, parsedTrace);
121121
const expectedData = '\n' +
122122
`
123123
@@ -170,7 +170,7 @@ self: 0.2
170170
if (!selectedEvent) {
171171
throw new Error('Could not find expected event.');
172172
}
173-
const callTree = Utils.AICallTree.AICallTree.from(selectedEvent, parsedTrace);
173+
const callTree = Utils.AICallTree.AICallTree.fromEvent(selectedEvent, parsedTrace);
174174
assert.isOk(callTree);
175175

176176
// We don't need to validate the whole tree, just that it has recursion
@@ -189,7 +189,7 @@ self: 0.2
189189
if (!tinyEvent) {
190190
throw new Error('Could not find expected event.');
191191
}
192-
const tinyStr = Utils.AICallTree.AICallTree.from(tinyEvent, parsedTrace)?.serialize();
192+
const tinyStr = Utils.AICallTree.AICallTree.fromEvent(tinyEvent, parsedTrace)?.serialize();
193193
assert.strictEqual(tinyStr?.split('\n').filter(l => l.startsWith('Node:')).join('\n'), `
194194
Node: 1 – Task
195195
Node: 2 – Parse HTML
@@ -203,7 +203,7 @@ Node: 5 – get storage`.trim());
203203
if (!evaluateEvent) {
204204
throw new Error('Could not find expected event.');
205205
}
206-
const treeStr = Utils.AICallTree.AICallTree.from(evaluateEvent, parsedTrace)?.serialize();
206+
const treeStr = Utils.AICallTree.AICallTree.fromEvent(evaluateEvent, parsedTrace)?.serialize();
207207
assert.strictEqual(treeStr?.split('\n').filter(l => l.startsWith('Node:')).join('\n'), `
208208
Node: 1 – Task
209209
Node: 2 – Parse HTML
@@ -218,7 +218,7 @@ Node: 6 – H.la`.trim());
218218
if (!compileEvent) {
219219
throw new Error('Could not find expected event.');
220220
}
221-
const compileStr = Utils.AICallTree.AICallTree.from(compileEvent, parsedTrace)?.serialize();
221+
const compileStr = Utils.AICallTree.AICallTree.fromEvent(compileEvent, parsedTrace)?.serialize();
222222
assert.strictEqual(compileStr?.split('\n').filter(l => l.startsWith('Node:')).join('\n'), `
223223
Node: 1 – Task
224224
Node: 2 – Parse HTML
@@ -227,33 +227,51 @@ Node: 4 – (anonymous)
227227
Node: 5 – Compile code`.trim());
228228
assert.include(compileStr, 'Compile code');
229229
});
230-
});
231230

232-
describe('AITreeFilter', () => {
233-
const makeEvent = (
234-
name: string,
235-
ts: number,
236-
dur: number,
237-
): Trace.Types.Events.Event => ({
238-
name,
239-
cat: 'disabled-by-default-devtools.timeline',
240-
ph: Trace.Types.Events.Phase.COMPLETE,
241-
ts: Trace.Types.Timing.Micro(ts),
242-
dur: Trace.Types.Timing.Micro(dur),
243-
pid: Trace.Types.Events.ProcessID(1),
244-
tid: Trace.Types.Events.ThreadID(4),
245-
args: {},
231+
it('can construct a tree from a period of time', async function() {
232+
const {parsedTrace} = await TraceLoader.traceEngine(this, 'nested-interactions.json.gz');
233+
// Picked this interaction event because it spans multiple icicles in the main thread.
234+
// Note: if you are debugging this test, it is useful to load up this trace
235+
// in RPP and look for the first "keydown" event.
236+
const interaction = parsedTrace.UserInteractions.interactionEventsWithNoNesting.find(e => {
237+
return Trace.Types.Events.isEventTimingStart(e.rawSourceEvent) &&
238+
e.rawSourceEvent.args.data.interactionId === 3572;
239+
});
240+
assert.isOk(interaction);
241+
const timings = Trace.Helpers.Timing.eventTimingsMicroSeconds(interaction);
242+
const tree = Utils.AICallTree.AICallTree.fromTime(timings.startTime, timings.endTime, parsedTrace);
243+
assert.isOk(tree);
244+
const output = tree.serialize();
245+
const totalNodes = output.split('\n').filter(l => l.startsWith('Node:')).length;
246+
assert.strictEqual(totalNodes, 242); // Check the min duration filter is working.
247+
// Check there are 3 keydown events. This confirms that the call tree is taking events from the right timespan.
248+
const keyDownEvents = output.split('\n').filter(line => {
249+
return line.startsWith('Node:') && line.includes('Event: keydown');
250+
});
251+
assert.lengthOf(keyDownEvents, 3);
246252
});
253+
});
247254

255+
const makeEvent = (name: string, ts: number, dur: number): Trace.Types.Events.Event => ({
256+
name,
257+
cat: 'disabled-by-default-devtools.timeline',
258+
ph: Trace.Types.Events.Phase.COMPLETE,
259+
ts: Trace.Types.Timing.Micro(ts),
260+
dur: Trace.Types.Timing.Micro(dur),
261+
pid: Trace.Types.Events.ProcessID(1),
262+
tid: Trace.Types.Events.ThreadID(4),
263+
args: {},
264+
});
265+
describe('AITreeFilter', () => {
248266
it('always includes the selected event', () => {
249267
const selectedEvent = makeEvent('selected', 0, 100);
250-
const filter = new Utils.AICallTree.AITreeFilter(selectedEvent);
268+
const filter = new Utils.AICallTree.SelectedEventDurationFilter(selectedEvent);
251269
assert.isTrue(filter.accept(selectedEvent));
252270
});
253271

254272
it('includes events that are long enough', () => {
255273
const selectedEvent = makeEvent('selected', 0, 100);
256-
const filter = new Utils.AICallTree.AITreeFilter(selectedEvent);
274+
const filter = new Utils.AICallTree.SelectedEventDurationFilter(selectedEvent);
257275

258276
assert.isTrue(filter.accept(makeEvent('short', 0, 1)));
259277
assert.isTrue(filter.accept(makeEvent('short', 0, 0.6)));
@@ -264,23 +282,25 @@ describe('AITreeFilter', () => {
264282

265283
it('excludes events that are too short', () => {
266284
const selectedEvent = makeEvent('selected', 0, 100);
267-
const filter = new Utils.AICallTree.AITreeFilter(selectedEvent);
285+
const filter = new Utils.AICallTree.SelectedEventDurationFilter(selectedEvent);
268286

269287
assert.isFalse(filter.accept(makeEvent('short', 0, 0)));
270288
assert.isFalse(filter.accept(makeEvent('short', 0, 0.1)));
271289
assert.isFalse(filter.accept(makeEvent('short', 0, 0.4)));
272290
});
291+
});
273292

293+
describe('CompileCode filter', () => {
274294
it('excludes COMPILE_CODE nodes if non-selected', () => {
275295
const selectedEvent = makeEvent('selected', 0, 100);
276296
const compileCodeEvent = makeEvent(Trace.Types.Events.Name.COMPILE_CODE, 0, 100);
277-
const filter = new Utils.AICallTree.AITreeFilter(selectedEvent);
297+
const filter = new Utils.AICallTree.ExcludeCompileCodeFilter(selectedEvent);
278298
assert.isFalse(filter.accept(compileCodeEvent));
279299
});
280300

281301
it('includes COMPILE_CODE nodes if selected', () => {
282302
const selectedEvent = makeEvent(Trace.Types.Events.Name.COMPILE_CODE, 0, 100);
283-
const filter = new Utils.AICallTree.AITreeFilter(selectedEvent);
303+
const filter = new Utils.AICallTree.ExcludeCompileCodeFilter(selectedEvent);
284304
assert.isTrue(filter.accept(selectedEvent));
285305
});
286306
});

0 commit comments

Comments
 (0)