Skip to content

Commit

Permalink
DrJones/Perf: Clean up old AINode impl
Browse files Browse the repository at this point in the history
No behaviour change. FYI: The modern impl is in AICallTree.

Bug: 370436840
Change-Id: Ifad37ea71a4ba8bc5c4d7249ec15327b7a6cd381
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6158779
Auto-Submit: Paul Irish <[email protected]>
Reviewed-by: Jack Franklin <[email protected]>
Commit-Queue: Jack Franklin <[email protected]>
  • Loading branch information
paulirish authored and Devtools-frontend LUCI CQ committed Jan 9, 2025
1 parent 5f1baa9 commit c182469
Show file tree
Hide file tree
Showing 5 changed files with 1 addition and 307 deletions.
70 changes: 0 additions & 70 deletions front_end/models/trace/helpers/TreeHelpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,74 +455,4 @@ describe('TreeHelpers', () => {
assert.isFalse(Trace.Helpers.TreeHelpers.canBuildTreesFromEvents(data));
});
});
describe('buildTreesFromEventsForAI', () => {
it('returns the trace entry tree starting from the root task, highlighting the selected event', () => {
const evaluateScript = makeCompleteEvent(Trace.Types.Events.Name.EVALUATE_SCRIPT, 0, 500);
const v8Run = makeCompleteEvent('v8.run', 10, 490);
const parseFunction = makeCompleteEvent('V8.ParseFunction', 12, 1);

const traceEvents: Trace.Types.Events.Event[] = [evaluateScript, v8Run, parseFunction];
const profileCalls = [makeProfileCall('a', 100, 200), makeProfileCall('b', 300, 200)];

// Roughly this looks like:
// 0 500
// |------------- EvaluateScript -------------|
// |- v8.run -|
// |--| |- a -||- b |
// ^ V8.ParseFunction

const allEntries = Trace.Helpers.Trace.mergeEventsInOrder(traceEvents, profileCalls);
const {tree, entryToNode} = Trace.Helpers.TreeHelpers.treify(allEntries, {filter: {has: () => true}});
const rootNode = entryToNode.get(evaluateScript);
// Select something from the middle.
const selectedNode = entryToNode.get(v8Run);

assert.strictEqual(tree.roots.size, 1);
assert.exists(rootNode);
assert.exists(selectedNode);

const aiNodeTree = Trace.Helpers.TreeHelpers.AINode.fromEntryNode(selectedNode, () => true);
const v8RunNode = Trace.Helpers.TreeHelpers.AINode.getSelectedNodeWithinTree(aiNodeTree);

assert.exists(aiNodeTree);
assert.exists(v8RunNode);

// First check the serialization as a while.
assert.deepEqual(JSON.parse(JSON.stringify(aiNodeTree)), {
name: 'EvaluateScript',
dur: 0.5,
self: 0,
children: [{
selected: true,
name: 'v8.run',
dur: 0.5,
self: 0.1,
children: [
{name: 'V8.ParseFunction', dur: 0, self: 0},
{name: 'a', url: '', dur: 0.2, self: 0.2},
{name: 'b', url: '', dur: 0.2, self: 0.2},
],
}],
});

// Now we can make sure the pre-serialized data is also correct.
assert.strictEqual(v8RunNode.name, 'v8.run');
assert.isTrue(v8RunNode.selected);
assert.strictEqual(v8RunNode.duration, 0.49);
assert.strictEqual(v8RunNode.children?.length, 3);

assert.strictEqual(aiNodeTree.name, 'EvaluateScript');
assert.isUndefined(aiNodeTree.selected);
assert.strictEqual(aiNodeTree.duration, 0.5);
assert.strictEqual(aiNodeTree.children?.length, 1);

const parseFnAINode = v8RunNode.children?.at(0) as Trace.Helpers.TreeHelpers.AINode;
assert.exists(v8RunNode);

assert.strictEqual(parseFnAINode.name, 'V8.ParseFunction');
assert.isUndefined(parseFnAINode.selected);
assert.strictEqual(parseFnAINode.duration, 0.001);
assert.isUndefined(parseFnAINode.children);
});
});
});
170 changes: 1 addition & 169 deletions front_end/models/trace/helpers/TreeHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import * as Types from '../types/types.js';

import {eventIsInBounds, microSecondsToMilliseconds} from './Timing.js';
import {eventIsInBounds} from './Timing.js';

let nodeIdCount = 0;
export const makeTraceEntryNodeId = (): TraceEntryNodeId => (++nodeIdCount) as TraceEntryNodeId;
Expand Down Expand Up @@ -37,174 +37,6 @@ export interface TraceEntryNode {
children: TraceEntryNode[];
}

export interface AINodeSerialized {
name: string;
dur?: number;
self?: number;
children?: AINodeSerialized[];
url?: string;
selected?: boolean;
}

/**
* Node in a graph simplified for AI Assistance processing. The graph mirrors the TraceEntryNode one.
* Huge tip of the hat to Victor Porof for prototyping this with some great work: https://crrev.com/c/5711249
*/
export class AINode {
// event: Types.Events.Event; // Set in the constructor.
name: string;
duration?: Types.Timing.MilliSeconds;
selfDuration?: Types.Timing.MilliSeconds;
id?: TraceEntryNodeId;
children?: AINode[];
url?: string;
selected?: boolean;

constructor(public event: Types.Events.Event) {
this.name = event.name;
this.duration = event.dur === undefined ? undefined : microSecondsToMilliseconds(event.dur);

if (Types.Events.isProfileCall(event)) {
this.name = event.callFrame.functionName || '(anonymous)';
this.url = event.callFrame.url;
}
}

// Manually handle how nodes in this tree are serialized. We'll drop serveral properties that we don't need in the JSON string.
// FYI: toJSON() is invoked implicitly via JSON.stringify()
toJSON(): AINodeSerialized {
return {
selected: this.selected,
name: this.name,
url: this.url,
// Round milliseconds because we don't need the precision
dur: this.duration === undefined ? undefined : Math.round(this.duration * 10) / 10,
self: this.selfDuration === undefined ? undefined : Math.round(this.selfDuration * 10) / 10,
children: this.children?.length ? this.children : undefined,
};
}

static #fromTraceEvent(event: Types.Events.Event): AINode {
return new AINode(event);
}

/**
* Builds a TraceEntryNodeForAI tree from a node and marks the selected node. Primary entrypoint from EntriesFilter
*/
static fromEntryNode(selectedNode: TraceEntryNode, entryIsVisibleInTimeline: (event: Types.Events.Event) => boolean):
AINode {
/**
* Builds a AINode tree from a TraceEntryNode tree and marks the selected node.
*/
function fromEntryNodeAndTree(node: TraceEntryNode): AINode {
const aiNode = AINode.#fromTraceEvent(node.entry);
aiNode.id = node.id;
if (node === selectedNode) {
aiNode.selected = true;
}
aiNode.selfDuration = node.selfTime === undefined ? undefined : microSecondsToMilliseconds(node.selfTime);
for (const child of node.children) {
aiNode.children ??= [];
aiNode.children.push(fromEntryNodeAndTree(child));
}
return aiNode;
}

function findTopMostVisibleAncestor(node: TraceEntryNode): TraceEntryNode {
const parentNodes = [node];
let parent = node.parent;
while (parent) {
parentNodes.unshift(parent);
parent = parent.parent;
}
return parentNodes.find(node => entryIsVisibleInTimeline(node.entry)) ?? node;
}

const topMostVisibleRoot = findTopMostVisibleAncestor(selectedNode);
const aiNode = fromEntryNodeAndTree(topMostVisibleRoot);

// If our root wasn't visible, this could return an array of multiple RunTasks.
// But with a visible root, we safely get back the exact same root, now with its descendent tree updated.
// Filter to ensure our tree here only has "visible" entries
const [filteredAiNodeRoot] = AINode.#filterRecursive([aiNode], node => {
if (node.event.name === 'V8.CompileCode' || node.event.name === 'UpdateCounters') {
return false;
}
return entryIsVisibleInTimeline(node.event);
});
return filteredAiNodeRoot;
}

static getSelectedNodeWithinTree(node: AINode): AINode|null {
if (node.selected) {
return node;
}
if (!node.children) {
return null;
}
for (const child of node.children) {
const returnedNode = AINode.getSelectedNodeWithinTree(child);
if (returnedNode) {
return returnedNode;
}
}
return null;
}

static #filterRecursive(list: AINode[], predicate: (node: AINode) => boolean): AINode[] {
let done;
do {
done = true;
const filtered: AINode[] = [];
for (const node of list) {
if (predicate(node)) {
// Keep it
filtered.push(node);
} else if (node.children) {
filtered.push(...node.children);
done = false;
}
}
list = filtered;
} while (!done);

for (const node of list) {
if (node.children) {
node.children = AINode.#filterRecursive(node.children, predicate);
}
}
return list;
}

static #removeInexpensiveNodesRecursively(
list: AINode[],
options?: {minDuration?: number, minSelf?: number, minJsDuration?: number, minJsSelf?: number}): AINode[] {
const minDuration = options?.minDuration ?? 0;
const minSelf = options?.minSelf ?? 0;
const minJsDuration = options?.minJsDuration ?? 0;
const minJsSelf = options?.minJsSelf ?? 0;

const isJS = (node: AINode): boolean => Boolean(node.url);
const longEnough = (node: AINode): boolean =>
node.duration === undefined || node.duration >= (isJS(node) ? minJsDuration : minDuration);
const selfLongEnough = (node: AINode): boolean =>
node.selfDuration === undefined || node.selfDuration >= (isJS(node) ? minJsSelf : minSelf);

return AINode.#filterRecursive(list, node => longEnough(node) && selfLongEnough(node));
}

// Invoked from PerformanceAgent
sanitize(): void {
if (this.children) {
this.children = AINode.#removeInexpensiveNodesRecursively(this.children, {
minDuration: Types.Timing.MilliSeconds(1),
minJsDuration: Types.Timing.MilliSeconds(1),
minJsSelf: Types.Timing.MilliSeconds(0.1),
});
}
}
}

class TraceEntryNodeIdTag {
/* eslint-disable-next-line no-unused-private-class-members */
readonly #tag: (symbol|undefined);
Expand Down
51 changes: 0 additions & 51 deletions front_end/panels/timeline/EntriesFilter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -700,55 +700,4 @@ describeWithEnvironment('EntriesFilter', function() {
assert.lengthOf(stack.expandableEntries(), 1);
assert.isTrue(stack.expandableEntries().includes(taskEntry));
});

it('returns the trace entry tree starting from the root task, highlighting the selected event', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'two-functions-recursion.json.gz');
const mainThread = getMainThread(parsedTrace.Renderer);
/** This stack looks roughly like so (with some events omitted):
* ===========RunTask===========
* ...
* ======== onclick ============
* =========== foo =============
* ==== foo2 =====
* ===== foo =====
* ==== foo2 =====
* ===== foo =====
* ==== foo2 =====
* ===== foo =====
*
* In this test we want to test if for a selected entry, the tree for AI processing
* is generated correctly such that the root RunTask is the root node and the
* node for the selected event has property selected set as true.
**/

const firstFooCallEntry = findFirstEntry(mainThread.entries, entry => {
return Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === 'foo' && entry.dur === 233;
});

const entriesFilter = new Timeline.EntriesFilter.EntriesFilter(parsedTrace.Renderer.entryToNode);
if (!entriesFilter) {
throw new Error('EntriesFilter does not exist');
}
const aiNodeTree = entriesFilter.getAIEventNodeTree(firstFooCallEntry);
assert.exists(aiNodeTree);

const fooAiNode = Trace.Helpers.TreeHelpers.AINode.getSelectedNodeWithinTree(aiNodeTree);
assert.exists(fooAiNode);

// Use the toJSON simplification for comparison.
const simpleFooNode = JSON.parse(JSON.stringify(fooAiNode));
assert.lengthOf(simpleFooNode.children, 1);

// delete for smaller deepStrictEqual comparison
simpleFooNode.children = [];

assert.deepEqual(simpleFooNode, {
dur: 0.2,
name: 'foo',
selected: true,
self: 0.2,
url: 'file:///usr/local/google/home/alinavarkki/stack/recursion.html',
children: [],
});
});
});
15 changes: 0 additions & 15 deletions front_end/panels/timeline/EntriesFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Platform from '../../core/platform/platform.js';
import * as Root from '../../core/root/root.js';
import * as Trace from '../../models/trace/trace.js';
import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js';

Expand Down Expand Up @@ -77,20 +76,6 @@ export class EntriesFilter {
return possibleActions;
}

/**
* Returns the trace entry tree for the specified event, simplified for input to AI Assistance.
* The tree is rooted at the top-level task that contains the event, with the node for specified event marked as selected.
*/
getAIEventNodeTree(entry: Trace.Types.Events.Event): Trace.Helpers.TreeHelpers.AINode|null {
const node = this.#entryToNode.get(entry);
if (!node) {
return null;
}

return Trace.Helpers.TreeHelpers.AINode.fromEntryNode(
node, Root.Runtime.experiments.isEnabled('timeline-show-all-events') ? () => true : entryIsVisibleInTimeline);
}

/**
* Returns the amount of entry descendants that belong to the hidden entries array.
* */
Expand Down
2 changes: 0 additions & 2 deletions front_end/ui/legacy/components/perf_ui/FlameChart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4199,8 +4199,6 @@ export interface FlameChartDataProvider {
// The following three functions are used for the flame chart entry customization.
modifyTree?(action: FilterAction, entryIndex: number): void;

getAIEventNodeTreeFromEntryIndex?(entryIndex: number): Trace.Helpers.TreeHelpers.AINode|null;

entryHasAnnotations?(entryIndex: number): boolean;

deleteAnnotationsForEntry?(entryIndex: number): void;
Expand Down

0 comments on commit c182469

Please sign in to comment.