Skip to content

Commit e46ebf2

Browse files
and-oliDevtools-frontend LUCI CQ
authored andcommitted
[RPP] Introduce StackTraceForEvent helper
It extracts the full call stack (including async calls) of a given event (at the moment only JS calls / ProfileCalls). We will use this to helper to display the call stack of a given event in its details. In the future support for extension entries will be added to it. Bug: 381392952 Change-Id: I1410b2ed0f85bb8f9e4799ae7b0606bbc7f247f5 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6074770 Reviewed-by: Jack Franklin <[email protected]> Commit-Queue: Andres Olivares <[email protected]>
1 parent 97238e1 commit e46ebf2

File tree

6 files changed

+324
-0
lines changed

6 files changed

+324
-0
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,7 @@ grd_files_debug_sources = [
10461046
"front_end/models/trace/extras/FilmStrip.js",
10471047
"front_end/models/trace/extras/MainThreadActivity.js",
10481048
"front_end/models/trace/extras/Metadata.js",
1049+
"front_end/models/trace/extras/StackTraceForEvent.js",
10491050
"front_end/models/trace/extras/ThirdParties.js",
10501051
"front_end/models/trace/extras/TimelineJSProfile.js",
10511052
"front_end/models/trace/extras/TraceFilter.js",

front_end/models/trace/extras/BUILD.gn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ devtools_module("extras") {
1313
"FilmStrip.ts",
1414
"MainThreadActivity.ts",
1515
"Metadata.ts",
16+
"StackTraceForEvent.ts",
1617
"ThirdParties.ts",
1718
"TimelineJSProfile.ts",
1819
"TraceFilter.ts",
@@ -48,6 +49,7 @@ ts_library("unittests") {
4849
"FilmStrip.test.ts",
4950
"MainThreadActivity.test.ts",
5051
"Metadata.test.ts",
52+
"StackTraceForEvent.test.ts",
5153
"ThirdParties.test.ts",
5254
"TraceFilter.test.ts",
5355
"TraceTree.test.ts",
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
// Copyright 2024 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import type * as Protocol from '../../../generated/protocol.js';
6+
import {describeWithEnvironment} from '../../../testing/EnvironmentHelpers.js';
7+
import {TraceLoader} from '../../../testing/TraceLoader.js';
8+
import * as Trace from '../trace.js';
9+
10+
function shapeStackTraceAsArray(stackTrace: Protocol.Runtime.StackTrace):
11+
{callFrames: Protocol.Runtime.CallFrame[], description?: string}[] {
12+
const stackTraceAsArray: {callFrames: Protocol.Runtime.CallFrame[], description?: string}[] = [];
13+
let currentStackTrace: Protocol.Runtime.StackTrace|undefined = stackTrace;
14+
while (currentStackTrace) {
15+
// @ts-ignore `codeType` is not included in the protocol types but
16+
// occasionally present
17+
currentStackTrace.callFrames.forEach(callFrame => delete callFrame.codeType);
18+
stackTraceAsArray.push({callFrames: currentStackTrace.callFrames, description: currentStackTrace.description});
19+
currentStackTrace = currentStackTrace.parent;
20+
}
21+
22+
return stackTraceAsArray;
23+
}
24+
describeWithEnvironment('StackTraceForTraceEvent', function() {
25+
let parsedTrace: Trace.Handlers.Types.ParsedTrace;
26+
beforeEach(async function() {
27+
const traceEngineData = await TraceLoader.traceEngine(this, 'async-js-calls.json.gz');
28+
parsedTrace = traceEngineData.parsedTrace;
29+
Trace.Extras.StackTraceForEvent.clearCacheForTrace(parsedTrace);
30+
});
31+
afterEach(async () => {
32+
Trace.Extras.StackTraceForEvent.clearCacheForTrace(parsedTrace);
33+
});
34+
35+
it('correctly builds the stack trace of a profile call when it only has a synchronous stack trace.',
36+
async function() {
37+
const jsCall = parsedTrace.Renderer.allTraceEntries.find(
38+
e => Trace.Types.Events.isProfileCall(e) && e.callFrame.functionName === 'startExample');
39+
assert.exists(jsCall);
40+
const stackTrace = Trace.Extras.StackTraceForEvent.get(jsCall, parsedTrace);
41+
assert.exists(stackTrace);
42+
const stackTraceArray = shapeStackTraceAsArray(stackTrace);
43+
assert.lengthOf(stackTraceArray, 1);
44+
const callFrames = stackTraceArray[0].callFrames;
45+
assert.deepEqual(callFrames, [
46+
{
47+
columnNumber: 21,
48+
functionName: 'startExample',
49+
lineNumber: 25,
50+
scriptId: '53' as Protocol.Runtime.ScriptId,
51+
url: '',
52+
},
53+
{columnNumber: 0, functionName: '', lineNumber: 0, scriptId: '53' as Protocol.Runtime.ScriptId, url: ''},
54+
]);
55+
});
56+
57+
it('correctly builds the stack trace of a profile call when it only has an asynchronous stack trace.',
58+
async function() {
59+
const jsCall = parsedTrace.Renderer.allTraceEntries.find(
60+
e => Trace.Types.Events.isProfileCall(e) && e.callFrame.functionName === 'baz');
61+
assert.exists(jsCall);
62+
const stackTrace = Trace.Extras.StackTraceForEvent.get(jsCall, parsedTrace);
63+
assert.exists(stackTrace);
64+
const stackTraceArray = shapeStackTraceAsArray(stackTrace);
65+
assert.lengthOf(stackTraceArray, 4);
66+
67+
assert.deepEqual(stackTraceArray, [
68+
{
69+
callFrames: [{
70+
columnNumber: 12,
71+
functionName: 'baz',
72+
lineNumber: 13,
73+
scriptId: '53' as Protocol.Runtime.ScriptId,
74+
url: '',
75+
}],
76+
description: undefined,
77+
},
78+
{
79+
callFrames: [{
80+
columnNumber: 12,
81+
functionName: 'bar',
82+
lineNumber: 6,
83+
scriptId: '53' as Protocol.Runtime.ScriptId,
84+
url: '',
85+
}],
86+
description: 'requestIdleCallback',
87+
},
88+
{
89+
callFrames: [{
90+
columnNumber: 12,
91+
functionName: 'foo',
92+
lineNumber: 0,
93+
scriptId: '53' as Protocol.Runtime.ScriptId,
94+
url: '',
95+
}],
96+
description: 'setTimeout',
97+
},
98+
{
99+
callFrames: [
100+
{
101+
columnNumber: 21,
102+
functionName: 'startExample',
103+
lineNumber: 25,
104+
scriptId: '53' as Protocol.Runtime.ScriptId,
105+
url: '',
106+
},
107+
{columnNumber: 0, functionName: '', lineNumber: 0, scriptId: '53', url: ''},
108+
],
109+
description: 'requestAnimationFrame',
110+
},
111+
]);
112+
});
113+
114+
it('correctly skips frames set to be ignored.', async function() {
115+
const jsCall = parsedTrace.Renderer.allTraceEntries.find(
116+
e => Trace.Types.Events.isProfileCall(e) && e.callFrame.functionName === 'baz');
117+
assert.exists(jsCall);
118+
const isIgnoreListedCallback = (e: Trace.Types.Events.Event) =>
119+
Trace.Types.Events.isProfileCall(e) && e.callFrame.functionName === 'bar';
120+
const stackTrace = Trace.Extras.StackTraceForEvent.get(jsCall, parsedTrace, {isIgnoreListedCallback});
121+
assert.exists(stackTrace);
122+
const stackTraceArray = shapeStackTraceAsArray(stackTrace);
123+
assert.lengthOf(stackTraceArray, 4);
124+
125+
assert.deepEqual(stackTraceArray, [
126+
{
127+
callFrames: [
128+
{columnNumber: 12, functionName: 'baz', lineNumber: 13, scriptId: '53' as Protocol.Runtime.ScriptId, url: ''},
129+
],
130+
description: undefined,
131+
},
132+
{callFrames: [], description: 'requestIdleCallback'},
133+
{
134+
callFrames: [
135+
{columnNumber: 12, functionName: 'foo', lineNumber: 0, scriptId: '53' as Protocol.Runtime.ScriptId, url: ''},
136+
],
137+
description: 'setTimeout',
138+
},
139+
{
140+
callFrames: [
141+
{
142+
columnNumber: 21,
143+
functionName: 'startExample',
144+
lineNumber: 25,
145+
scriptId: '53' as Protocol.Runtime.ScriptId,
146+
url: '',
147+
},
148+
{columnNumber: 0, functionName: '', lineNumber: 0, scriptId: '53', url: ''},
149+
],
150+
description: 'requestAnimationFrame',
151+
},
152+
]);
153+
});
154+
155+
it('uses cached data correctly.', async function() {
156+
const fooCall = parsedTrace.Renderer.allTraceEntries.find(
157+
e => Trace.Types.Events.isProfileCall(e) && e.callFrame.functionName === 'foo');
158+
assert.exists(fooCall);
159+
const result =
160+
parsedTrace.AsyncJSCalls.asyncCallToScheduler.get(fooCall as Trace.Types.Events.SyntheticProfileCall);
161+
assert.exists(result);
162+
const {scheduler: parentOfFoo} = result;
163+
164+
// Compute stack trace of foo's parent
165+
const stackTraceOfParent = Trace.Extras.StackTraceForEvent.get(parentOfFoo, parsedTrace);
166+
assert.exists(stackTraceOfParent);
167+
const stackTraceArray = shapeStackTraceAsArray(stackTraceOfParent);
168+
assert.lengthOf(stackTraceArray, 1);
169+
170+
// Modify the cache, to check it's used when possible
171+
const bottomFrame = stackTraceOfParent.callFrames.at(-1);
172+
assert.exists(bottomFrame);
173+
bottomFrame.functionName = 'Overriden name';
174+
175+
// Compute stack trace of foo, ensure the cache calculated with
176+
// its parent is used.
177+
const stackTraceOfFoo = Trace.Extras.StackTraceForEvent.get(fooCall, parsedTrace);
178+
assert.exists(stackTraceOfFoo);
179+
const stackTraceArray2 = shapeStackTraceAsArray(stackTraceOfFoo);
180+
assert.deepEqual(stackTraceArray2, [
181+
{
182+
callFrames: [
183+
{columnNumber: 12, functionName: 'foo', lineNumber: 0, scriptId: '53' as Protocol.Runtime.ScriptId, url: ''},
184+
],
185+
description: undefined,
186+
},
187+
{
188+
callFrames: [
189+
{
190+
columnNumber: 21,
191+
functionName: 'startExample',
192+
lineNumber: 25,
193+
scriptId: '53' as Protocol.Runtime.ScriptId,
194+
url: '',
195+
},
196+
{
197+
columnNumber: 0,
198+
functionName: bottomFrame.functionName,
199+
lineNumber: 0,
200+
scriptId: '53' as Protocol.Runtime.ScriptId,
201+
url: '',
202+
},
203+
],
204+
description: 'requestAnimationFrame',
205+
},
206+
]);
207+
});
208+
});
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright 2024 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import type * as Protocol from '../../../generated/protocol.js';
6+
import type * as Handlers from '../handlers/handlers.js';
7+
import type * as Helpers from '../helpers/helpers.js';
8+
import * as Types from '../types/types.js';
9+
10+
export const stackTraceForEventInTrace =
11+
new Map<Handlers.Types.ParsedTrace, Map<Types.Events.Event, Protocol.Runtime.StackTrace>>();
12+
13+
export function clearCacheForTrace(parsedTrace: Handlers.Types.ParsedTrace): void {
14+
stackTraceForEventInTrace.delete(parsedTrace);
15+
}
16+
export function get(
17+
event: Types.Events.Event, parsedTrace: Handlers.Types.ParsedTrace,
18+
options?: {isIgnoreListedCallback?: (event: Types.Events.Event) => boolean}): Protocol.Runtime.StackTrace|null {
19+
let cacheForTrace = stackTraceForEventInTrace.get(parsedTrace);
20+
if (!cacheForTrace) {
21+
cacheForTrace = new Map();
22+
stackTraceForEventInTrace.set(parsedTrace, cacheForTrace);
23+
}
24+
const resultFromCache = cacheForTrace.get(event);
25+
if (resultFromCache) {
26+
return resultFromCache;
27+
}
28+
if (!Types.Events.isProfileCall(event)) {
29+
return null;
30+
}
31+
const result = getForProfileCall(event, parsedTrace, options);
32+
cacheForTrace.set(event, result);
33+
return result;
34+
}
35+
36+
function getForProfileCall(
37+
event: Types.Events.SyntheticProfileCall, parsedTrace: Handlers.Types.ParsedTrace,
38+
options?: {isIgnoreListedCallback?: (event: Types.Events.Event) => boolean}): Protocol.Runtime.StackTrace {
39+
// When working with a CPU profile the renderer handler won't have
40+
// entries in its tree.
41+
const entryToNode =
42+
parsedTrace.Renderer.entryToNode.size > 0 ? parsedTrace.Renderer.entryToNode : parsedTrace.Samples.entryToNode;
43+
const topStackTrace: Protocol.Runtime.StackTrace = {callFrames: []};
44+
let stackTrace: Protocol.Runtime.StackTrace = topStackTrace;
45+
let currentEntry = event;
46+
let node: Helpers.TreeHelpers.TraceEntryNode|null|undefined = entryToNode.get(event);
47+
const traceCache =
48+
stackTraceForEventInTrace.get(parsedTrace) || new Map<Types.Events.Event, Protocol.Runtime.StackTrace>();
49+
stackTraceForEventInTrace.set(parsedTrace, traceCache);
50+
// Move up this node's ancestor tree appending frames to its
51+
// stack trace.
52+
while (node) {
53+
if (!Types.Events.isProfileCall(node.entry)) {
54+
node = node.parent;
55+
continue;
56+
}
57+
58+
currentEntry = node.entry;
59+
// First check if this entry was processed before.
60+
const stackTraceFromCache = traceCache.get(node.entry);
61+
if (stackTraceFromCache) {
62+
stackTrace.callFrames.push(...stackTraceFromCache.callFrames.filter(callFrame => !isNativeJSFunction(callFrame)));
63+
stackTrace.parent = stackTraceFromCache.parent;
64+
// Only set the description to the cache value if we didn't
65+
// compute it in the previous iteration, since the async stack
66+
// trace descriptions / taskNames is only extracted when jumping
67+
// to the async parent, and that might not have happened when
68+
// the cached value was computed (e.g. the cached value
69+
// computation started at some point inside the parent stack
70+
// trace).
71+
stackTrace.description = stackTrace.description || stackTraceFromCache.description;
72+
break;
73+
}
74+
75+
const ignorelisted = options?.isIgnoreListedCallback && options?.isIgnoreListedCallback(currentEntry);
76+
if (!ignorelisted && !isNativeJSFunction(currentEntry.callFrame)) {
77+
stackTrace.callFrames.push(currentEntry.callFrame);
78+
}
79+
const maybeAsyncParentEvent = parsedTrace.AsyncJSCalls.asyncCallToScheduler.get(currentEntry);
80+
const maybeAsyncParentNode = maybeAsyncParentEvent && entryToNode.get(maybeAsyncParentEvent.scheduler);
81+
if (maybeAsyncParentNode) {
82+
// The Protocol.Runtime.StackTrace type is recursive, so we
83+
// move one level deeper in it as we walk up the ancestor tree.
84+
stackTrace.parent = {callFrames: []};
85+
stackTrace = stackTrace.parent;
86+
// Note: this description effectively corresponds to the name
87+
// of the task that scheduled the stack trace we are jumping
88+
// FROM, so it would make sense that it was set to that stack
89+
// trace instead of the one we are jumping TO. However, the
90+
// JS presentation utils we use to present async stack traces
91+
// assume the description is added to the stack trace that
92+
// scheduled the async task, so we build the data that way.
93+
stackTrace.description = maybeAsyncParentEvent.taskName;
94+
node = maybeAsyncParentNode;
95+
continue;
96+
}
97+
node = node.parent;
98+
}
99+
return topStackTrace;
100+
}
101+
/**
102+
* Determines if a function is a native JS API (like setTimeout,
103+
* requestAnimationFrame, consoleTask.run. etc.). This is useful to
104+
* discard stack frames corresponding to the JS scheduler function
105+
* itself, since it's already being used as title of async stack traces
106+
* taken from the async `taskName`. This is also consistent with the
107+
* behaviour of the stack trace in the sources
108+
* panel.
109+
*/
110+
function isNativeJSFunction({columnNumber, lineNumber, url, scriptId}: Protocol.Runtime.CallFrame): boolean {
111+
return lineNumber === -1 && columnNumber === -1 && url === '' && scriptId === '0';
112+
}

front_end/models/trace/extras/extras.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export * as FetchNodes from './FetchNodes.js';
66
export * as FilmStrip from './FilmStrip.js';
77
export * as MainThreadActivity from './MainThreadActivity.js';
88
export * as Metadata from './Metadata.js';
9+
export * as StackTraceForEvent from './StackTraceForEvent.js';
910
export * as ThirdParties from './ThirdParties.js';
1011
export * as TimelineJSProfile from './TimelineJSProfile.js';
1112
export * as TraceFilter from './TraceFilter.js';
74 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)