Skip to content

Commit 773b5a9

Browse files
paulirishDevtools-frontend LUCI CQ
authored andcommitted
[DrJones/Perf] Aggressively filter trace stack to fit in context window
Many actual production stacks would blow our token window size. Victor's epic 'throwaway' code (https://crrev.com/c/5711249) leads the charge again for tree manipulation and culling of unimportant nodes. We also create a custom serialized representation, enabling rich debugging along with a svelte wire format. Post-submit code-reviews very welcome. Change-Id: I8491d0cbf294527d655e0b28df12056d642d6774 Bug: 370436840, 373543522 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/5950605 Reviewed-by: Connor Clark <[email protected]> Commit-Queue: Paul Irish <[email protected]>
1 parent d76a271 commit 773b5a9

File tree

9 files changed

+275
-175
lines changed

9 files changed

+275
-175
lines changed

front_end/models/trace/helpers/TreeHelpers.test.ts

Lines changed: 52 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -462,48 +462,67 @@ describe('TreeHelpers', () => {
462462
const parseFunction = makeCompleteEvent('V8.ParseFunction', 12, 1);
463463

464464
const traceEvents: Trace.Types.Events.Event[] = [evaluateScript, v8Run, parseFunction];
465-
466465
const profileCalls = [makeProfileCall('a', 100, 200), makeProfileCall('b', 300, 200)];
466+
467+
// Roughly this looks like:
468+
// 0 500
469+
// |------------- EvaluateScript -------------|
470+
// |- v8.run -|
471+
// |--| |- a -||- b |
472+
// ^ V8.ParseFunction
473+
467474
const allEntries = Trace.Helpers.Trace.mergeEventsInOrder(traceEvents, profileCalls);
468475
const {tree, entryToNode} = Trace.Helpers.TreeHelpers.treify(allEntries, {filter: {has: () => true}});
469476
const rootNode = entryToNode.get(evaluateScript);
470-
const selectedNode = entryToNode.get(parseFunction);
477+
// Select something from the middle.
478+
const selectedNode = entryToNode.get(v8Run);
471479

472480
assert.strictEqual(tree.roots.size, 1);
473481
assert.exists(rootNode);
474482
assert.exists(selectedNode);
475483

476-
const traceEntryTreeForAI = Trace.Helpers.TreeHelpers.AINode.fromEntryNode(selectedNode);
477-
const actualSelectedNode = Trace.Helpers.TreeHelpers.AINode.getSelectedNodeWithinTree(traceEntryTreeForAI);
478-
479-
assert.exists(traceEntryTreeForAI);
480-
assert.exists(actualSelectedNode);
481-
482-
// delete for smaller deepStrictEqual comparison
483-
actualSelectedNode.children = traceEntryTreeForAI.children = [];
484-
485-
const expectedTraceEntryTree = new Trace.Helpers.TreeHelpers.AINode(
486-
'EvaluateScript',
487-
Trace.Types.Timing.MilliSeconds(0),
488-
Trace.Types.Timing.MilliSeconds(0.5),
489-
undefined,
490-
Trace.Types.Timing.MilliSeconds(0.01),
491-
);
492-
expectedTraceEntryTree.id = 0 as Trace.Helpers.TreeHelpers.TraceEntryNodeId;
493-
expectedTraceEntryTree.children = [];
494-
assert.deepStrictEqual(traceEntryTreeForAI, expectedTraceEntryTree);
495-
496-
const expectedselectedNodeForAI = new Trace.Helpers.TreeHelpers.AINode(
497-
'V8.ParseFunction',
498-
Trace.Types.Timing.MilliSeconds(0.012),
499-
Trace.Types.Timing.MilliSeconds(0.001),
500-
undefined,
501-
Trace.Types.Timing.MilliSeconds(0.001),
502-
);
503-
expectedselectedNodeForAI.id = 2 as Trace.Helpers.TreeHelpers.TraceEntryNodeId;
504-
expectedselectedNodeForAI.children = [];
505-
expectedselectedNodeForAI.selected = true;
506-
assert.deepStrictEqual(actualSelectedNode, expectedselectedNodeForAI);
484+
const aiNodeTree = Trace.Helpers.TreeHelpers.AINode.fromEntryNode(selectedNode, () => true);
485+
const v8RunNode = Trace.Helpers.TreeHelpers.AINode.getSelectedNodeWithinTree(aiNodeTree);
486+
487+
assert.exists(aiNodeTree);
488+
assert.exists(v8RunNode);
489+
490+
// First check the serialization as a while.
491+
assert.deepStrictEqual(JSON.parse(JSON.stringify(aiNodeTree)), {
492+
name: 'EvaluateScript',
493+
dur: 0.5,
494+
self: 0,
495+
children: [{
496+
selected: true,
497+
name: 'v8.run',
498+
dur: 0.5,
499+
self: 0.1,
500+
children: [
501+
{name: 'V8.ParseFunction', dur: 0, self: 0},
502+
{name: 'a', url: '', dur: 0.2, self: 0.2},
503+
{name: 'b', url: '', dur: 0.2, self: 0.2},
504+
],
505+
}],
506+
});
507+
508+
// Now we can make sure the pre-serialized data is also correct.
509+
assert.strictEqual(v8RunNode.name, 'v8.run');
510+
assert.strictEqual(v8RunNode.selected, true);
511+
assert.strictEqual(v8RunNode.duration, 0.49);
512+
assert.strictEqual(v8RunNode.children?.length, 3);
513+
514+
assert.strictEqual(aiNodeTree.name, 'EvaluateScript');
515+
assert.strictEqual(aiNodeTree.selected, undefined);
516+
assert.strictEqual(aiNodeTree.duration, 0.5);
517+
assert.strictEqual(aiNodeTree.children?.length, 1);
518+
519+
const parseFnAINode = v8RunNode.children?.at(0) as Trace.Helpers.TreeHelpers.AINode;
520+
assert.exists(v8RunNode);
521+
522+
assert.strictEqual(parseFnAINode.name, 'V8.ParseFunction');
523+
assert.strictEqual(parseFnAINode.selected, undefined);
524+
assert.strictEqual(parseFnAINode.duration, 0.001);
525+
assert.strictEqual(parseFnAINode.children, undefined);
507526
});
508527
});
509528
});

front_end/models/trace/helpers/TreeHelpers.ts

Lines changed: 132 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -37,64 +37,102 @@ export interface TraceEntryNode {
3737
children: TraceEntryNode[];
3838
}
3939

40-
/** Node in a graph simplified for AI Assistance processing. The graph mirrors the TraceEntryNode one. */
40+
export interface AINodeSerialized {
41+
name: string;
42+
dur?: number;
43+
self?: number;
44+
children?: AINodeSerialized[];
45+
url?: string;
46+
selected?: boolean;
47+
}
48+
49+
/**
50+
* Node in a graph simplified for AI Assistance processing. The graph mirrors the TraceEntryNode one.
51+
* Huge tip of the hat to Victor Porof for prototyping this with some great work: https://crrev.com/c/5711249
52+
*/
4153
export class AINode {
54+
// event: Types.Events.Event; // Set in the constructor.
55+
name: string;
56+
duration?: Types.Timing.MilliSeconds;
57+
selfDuration?: Types.Timing.MilliSeconds;
4258
id?: TraceEntryNodeId;
43-
domain?: string;
44-
line?: number;
45-
column?: number;
46-
function?: string;
4759
children?: AINode[];
60+
url?: string;
4861
selected?: boolean;
4962

50-
constructor(
51-
public type: string, public start: Types.Timing.MilliSeconds, public end?: Types.Timing.MilliSeconds,
52-
public totalTime?: Types.Timing.MilliSeconds, public selfTime?: Types.Timing.MilliSeconds) {
53-
}
63+
constructor(public event: Types.Events.Event) {
64+
this.name = event.name;
65+
this.duration = event.dur === undefined ? undefined : microSecondsToMilliseconds(event.dur);
5466

55-
static #fromTraceEvent(event: Types.Events.Event): AINode {
56-
const start = microSecondsToMilliseconds(event.ts);
57-
const duration = event.dur === undefined ? undefined : microSecondsToMilliseconds(event.dur);
58-
const aiNode = new AINode(event.name, start, duration);
5967
if (Types.Events.isProfileCall(event)) {
60-
aiNode.function = event.callFrame.functionName || '(anonymous)';
61-
try {
62-
const url = new URL(event.callFrame.url);
63-
aiNode.domain = url.origin;
64-
aiNode.line = event.callFrame.lineNumber;
65-
aiNode.column = event.callFrame.columnNumber;
66-
} catch (e) {
67-
}
68+
this.name = event.callFrame.functionName || '(anonymous)';
69+
this.url = event.callFrame.url;
6870
}
69-
return aiNode;
71+
}
72+
73+
// Manually handle how nodes in this tree are serialized. We'll drop serveral properties that we don't need in the JSON string.
74+
// FYI: toJSON() is invoked implicitly via JSON.stringify()
75+
toJSON(): AINodeSerialized {
76+
return {
77+
selected: this.selected,
78+
name: this.name,
79+
url: this.url,
80+
// Round milliseconds because we don't need the precision
81+
dur: this.duration === undefined ? undefined : Math.round(this.duration * 10) / 10,
82+
self: this.selfDuration === undefined ? undefined : Math.round(this.selfDuration * 10) / 10,
83+
children: this.children?.length ? this.children : undefined,
84+
};
85+
}
86+
87+
static #fromTraceEvent(event: Types.Events.Event): AINode {
88+
return new AINode(event);
7089
}
7190

7291
/**
73-
* Builds a AINode tree from a TraceEntryNode tree and marks the selected node.
92+
* Builds a TraceEntryNodeForAI tree from a node and marks the selected node. Primary entrypoint from EntriesFilter
7493
*/
75-
static #fromEntryNodeAndTree(node: TraceEntryNode, selectedNode: TraceEntryNode): AINode {
76-
const aiNode = AINode.#fromTraceEvent(node.entry);
77-
aiNode.id = node.id;
78-
if (node === selectedNode) {
79-
aiNode.selected = true;
80-
}
81-
aiNode.selfTime = node.selfTime === undefined ? undefined : microSecondsToMilliseconds(node.selfTime);
82-
for (const child of node.children) {
83-
aiNode.children ??= [];
84-
aiNode.children.push(AINode.#fromEntryNodeAndTree(child, selectedNode));
94+
static fromEntryNode(selectedNode: TraceEntryNode, entryIsVisibleInTimeline: (event: Types.Events.Event) => boolean):
95+
AINode {
96+
/**
97+
* Builds a AINode tree from a TraceEntryNode tree and marks the selected node.
98+
*/
99+
function fromEntryNodeAndTree(node: TraceEntryNode): AINode {
100+
const aiNode = AINode.#fromTraceEvent(node.entry);
101+
aiNode.id = node.id;
102+
if (node === selectedNode) {
103+
aiNode.selected = true;
104+
}
105+
aiNode.selfDuration = node.selfTime === undefined ? undefined : microSecondsToMilliseconds(node.selfTime);
106+
for (const child of node.children) {
107+
aiNode.children ??= [];
108+
aiNode.children.push(fromEntryNodeAndTree(child));
109+
}
110+
return aiNode;
85111
}
86-
return aiNode;
87-
}
88112

89-
static fromEntryNode(selectedNode: TraceEntryNode): AINode {
90-
function getRoot(node: TraceEntryNode): TraceEntryNode {
91-
if (node.parent) {
92-
return getRoot(node.parent);
113+
function findTopMostVisibleAncestor(node: TraceEntryNode): TraceEntryNode {
114+
const parentNodes = [node];
115+
let parent = node.parent;
116+
while (parent) {
117+
parentNodes.unshift(parent);
118+
parent = parent.parent;
93119
}
94-
return node;
120+
return parentNodes.find(node => entryIsVisibleInTimeline(node.entry)) ?? node;
95121
}
96122

97-
return AINode.#fromEntryNodeAndTree(getRoot(selectedNode), selectedNode);
123+
const topMostVisibleRoot = findTopMostVisibleAncestor(selectedNode);
124+
const aiNode = fromEntryNodeAndTree(topMostVisibleRoot);
125+
126+
// If our root wasn't visible, this could return an array of multiple RunTasks.
127+
// But with a visible root, we safely get back the exact same root, now with its descendent tree updated.
128+
// Filter to ensure our tree here only has "visible" entries
129+
const [filteredAiNodeRoot] = AINode.#filterRecursive([aiNode], node => {
130+
if (node.event.name === 'V8.CompileCode' || node.event.name === 'UpdateCounters') {
131+
return false;
132+
}
133+
return entryIsVisibleInTimeline(node.event);
134+
});
135+
return filteredAiNodeRoot;
98136
}
99137

100138
static getSelectedNodeWithinTree(node: AINode): AINode|null {
@@ -112,6 +150,59 @@ export class AINode {
112150
}
113151
return null;
114152
}
153+
154+
static #filterRecursive(list: AINode[], predicate: (node: AINode) => boolean): AINode[] {
155+
let done;
156+
do {
157+
done = true;
158+
const filtered: AINode[] = [];
159+
for (const node of list) {
160+
if (predicate(node)) {
161+
// Keep it
162+
filtered.push(node);
163+
} else if (node.children) {
164+
filtered.push(...node.children);
165+
done = false;
166+
}
167+
}
168+
list = filtered;
169+
} while (!done);
170+
171+
for (const node of list) {
172+
if (node.children) {
173+
node.children = AINode.#filterRecursive(node.children, predicate);
174+
}
175+
}
176+
return list;
177+
}
178+
179+
static #removeInexpensiveNodesRecursively(
180+
list: AINode[],
181+
options?: {minDuration?: number, minSelf?: number, minJsDuration?: number, minJsSelf?: number}): AINode[] {
182+
const minDuration = options?.minDuration ?? 0;
183+
const minSelf = options?.minSelf ?? 0;
184+
const minJsDuration = options?.minJsDuration ?? 0;
185+
const minJsSelf = options?.minJsSelf ?? 0;
186+
187+
const isJS = (node: AINode): boolean => Boolean(node.url);
188+
const longEnough = (node: AINode): boolean =>
189+
node.duration === undefined || node.duration >= (isJS(node) ? minJsDuration : minDuration);
190+
const selfLongEnough = (node: AINode): boolean =>
191+
node.selfDuration === undefined || node.selfDuration >= (isJS(node) ? minJsSelf : minSelf);
192+
193+
return AINode.#filterRecursive(list, node => longEnough(node) && selfLongEnough(node));
194+
}
195+
196+
// Invoked from DrJonesPerformanceAgent
197+
sanitize(): void {
198+
if (this.children) {
199+
this.children = AINode.#removeInexpensiveNodesRecursively(this.children, {
200+
minDuration: Types.Timing.MilliSeconds(1),
201+
minJsDuration: Types.Timing.MilliSeconds(1),
202+
minJsSelf: Types.Timing.MilliSeconds(0.1),
203+
});
204+
}
205+
}
115206
}
116207

117208
class TraceEntryNodeIdTag {

0 commit comments

Comments
 (0)