Skip to content

Commit c74bc01

Browse files
and-oliDevtools-frontend LUCI CQ
authored andcommitted
[RPP] Show full stack traces for extension events
The StackTraceForEvent helper now supports extension entries, and its output is used in their details. Just like with profile calls, we want the full stack trace, including async calls. For this reason we don't use only the stack trace available in the trace event, since that contains only the synchronous function. Frameworks and third parties in general use async tasks schedulers, meaning that to improve attribution to authored source code in extension entries support for async call stacks is needed. To build the async call stack of an extension entry, we look for the JS function call (ProfileCall) where the call to the API was made (aka the call point to performance.mark/measure), then take the callstack of this profile call as the call stack of the extension entry. The assignment of the profile call to the extension entry is done based on the (sync) stack trace available in the extension entry trace event and on the time the API was called, this data is used to look for the closest matching profile call. Bug: 381861194 Change-Id: Ia3cfa9d12ecc6d6d7fcabb9faaa0e0f39613dab6 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6084695 Commit-Queue: Andres Olivares <[email protected]> Reviewed-by: Jack Franklin <[email protected]>
1 parent a7b5e8c commit c74bc01

File tree

8 files changed

+202
-35
lines changed

8 files changed

+202
-35
lines changed

front_end/models/trace/extras/StackTraceForEvent.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,4 +165,95 @@ describeWithEnvironment('StackTraceForTraceEvent', function() {
165165
]);
166166
bottomFrame.functionName = originalName;
167167
});
168+
it('correctly builds the stack trace of an extension entry', async function() {
169+
const jsCall = parsedTrace.Renderer.allTraceEntries.find(
170+
e => Trace.Types.Events.isProfileCall(e) && e.callFrame.functionName === 'baz') as
171+
Trace.Types.Events.SyntheticProfileCall |
172+
undefined;
173+
assert.exists(jsCall);
174+
const stackTraceForExtensionProfileCall = Trace.Extras.StackTraceForEvent.get(jsCall, parsedTrace);
175+
assert.exists(stackTraceForExtensionProfileCall);
176+
177+
// Create an extension entry right next to our profile call (based
178+
// on its callTime property).
179+
// Test the profile call's stack strace is returned as the
180+
// extension entry's stack trace.
181+
const mockExtensionEntry = {
182+
cat: 'devtools.extension',
183+
ts: jsCall.ts,
184+
pid: jsCall.pid,
185+
tid: jsCall.tid,
186+
rawSourceEvent: {
187+
cat: 'blink.user_timing',
188+
args: {stackTrace: [{functionName: jsCall.callFrame.functionName}], callTime: jsCall.ts - 1},
189+
ph: Trace.Types.Events.Phase.ASYNC_NESTABLE_START,
190+
},
191+
} as Trace.Types.Extensions.SyntheticExtensionEntry;
192+
const stackTraceForExtensionEntry = Trace.Extras.StackTraceForEvent.get(mockExtensionEntry, parsedTrace);
193+
assert.exists(stackTraceForExtensionEntry);
194+
195+
assert.strictEqual(stackTraceForExtensionEntry, stackTraceForExtensionProfileCall);
196+
});
197+
it('uses the stack trace of the profile call that contains an extension entry call time', async function() {
198+
const bar = parsedTrace.Renderer.allTraceEntries.find(
199+
e => Trace.Types.Events.isProfileCall(e) && e.callFrame.functionName === 'bar') as
200+
Trace.Types.Events.SyntheticProfileCall |
201+
undefined;
202+
assert.exists(bar);
203+
const stackTraceForExtensionProfileCall = Trace.Extras.StackTraceForEvent.get(bar, parsedTrace);
204+
assert.exists(stackTraceForExtensionProfileCall);
205+
206+
// Create an extension entry contained by the profile call (based on
207+
// its callTime property).
208+
// Test the profile call's stack strace is returned as
209+
// the extension entry's stack trace.
210+
const mockExtensionEntry = {
211+
cat: 'devtools.extension',
212+
ts: bar.ts,
213+
pid: bar.pid,
214+
tid: bar.tid,
215+
rawSourceEvent: {
216+
cat: 'blink.user_timing',
217+
args: {stackTrace: [{functionName: bar.callFrame.functionName}], callTime: bar.ts + 1},
218+
ph: Trace.Types.Events.Phase.ASYNC_NESTABLE_START,
219+
},
220+
} as Trace.Types.Extensions.SyntheticExtensionEntry;
221+
const stackTraceForExtensionEntry = Trace.Extras.StackTraceForEvent.get(mockExtensionEntry, parsedTrace);
222+
assert.exists(stackTraceForExtensionEntry);
223+
224+
assert.strictEqual(stackTraceForExtensionEntry, stackTraceForExtensionProfileCall);
225+
});
226+
227+
it('picks the stack trace of the closest profile call when no profile call contains the extension entry call time',
228+
async function() {
229+
const bazCalls = parsedTrace.Renderer.allTraceEntries.filter(
230+
e => Trace.Types.Events.isProfileCall(e) && e.callFrame.functionName === 'baz');
231+
const firstBaz = bazCalls.at(0);
232+
assert.exists(firstBaz);
233+
const secondBaz = bazCalls.at(1) as Trace.Types.Events.SyntheticProfileCall;
234+
assert.exists(secondBaz);
235+
const middlePoint = (secondBaz.ts + firstBaz.ts) / 2;
236+
237+
const stackTraceForSecondBaz = Trace.Extras.StackTraceForEvent.get(secondBaz, parsedTrace);
238+
assert.exists(stackTraceForSecondBaz);
239+
240+
// Create an extension entry contained closer to the second
241+
// baz call (based on its callTime property).
242+
// Test the stack trace of baz is used for it.
243+
const mockExtensionEntry = {
244+
cat: 'devtools.extension',
245+
ts: middlePoint,
246+
pid: secondBaz.pid,
247+
tid: secondBaz.tid,
248+
rawSourceEvent: {
249+
cat: 'blink.user_timing',
250+
args: {stackTrace: [{functionName: secondBaz.callFrame.functionName}], callTime: middlePoint + 1},
251+
ph: Trace.Types.Events.Phase.ASYNC_NESTABLE_START,
252+
},
253+
} as Trace.Types.Extensions.SyntheticExtensionEntry;
254+
const stackTraceForExtensionEntry = Trace.Extras.StackTraceForEvent.get(mockExtensionEntry, parsedTrace);
255+
assert.exists(stackTraceForExtensionEntry);
256+
257+
assert.strictEqual(stackTraceForExtensionEntry, stackTraceForSecondBaz);
258+
});
168259
});

front_end/models/trace/extras/StackTraceForEvent.ts

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import * as Platform from '../../../core/platform/platform.js';
56
import type * as Protocol from '../../../generated/protocol.js';
67
import type * as Handlers from '../handlers/handlers.js';
7-
import type * as Helpers from '../helpers/helpers.js';
8+
import * as Helpers from '../helpers/helpers.js';
89
import * as Types from '../types/types.js';
910

1011
export const stackTraceForEventInTrace =
@@ -24,11 +25,15 @@ export function get(event: Types.Events.Event, parsedTrace: Handlers.Types.Parse
2425
if (resultFromCache) {
2526
return resultFromCache;
2627
}
27-
if (!Types.Events.isProfileCall(event)) {
28-
return null;
28+
let result: Protocol.Runtime.StackTrace|null = null;
29+
if (Types.Events.isProfileCall(event)) {
30+
result = getForProfileCall(event, parsedTrace);
31+
} else if (Types.Extensions.isSyntheticExtensionEntry(event)) {
32+
result = getForExtensionEntry(event, parsedTrace);
33+
}
34+
if (result) {
35+
cacheForTrace.set(event, result);
2936
}
30-
const result = getForProfileCall(event, parsedTrace);
31-
cacheForTrace.set(event, result);
3237
return result;
3338
}
3439

@@ -95,6 +100,59 @@ function getForProfileCall(
95100
}
96101
return topStackTrace;
97102
}
103+
104+
/**
105+
* Finds the JS call in which an extension entry was injected (the
106+
* code location that called the extension API), and returns its stack
107+
* trace.
108+
*/
109+
function getForExtensionEntry(event: Types.Extensions.SyntheticExtensionEntry, parsedTrace: Handlers.Types.ParsedTrace):
110+
Protocol.Runtime.StackTrace|null {
111+
const eventCallPoint = Helpers.Trace.getZeroIndexedStackTraceForEvent(event)?.[0];
112+
if (!eventCallPoint) {
113+
return null;
114+
}
115+
const eventCallTime = Types.Events.isPerformanceMeasureBegin(event.rawSourceEvent) ?
116+
event.rawSourceEvent.args.callTime :
117+
event.rawSourceEvent.args.data?.callTime;
118+
if (eventCallTime === undefined) {
119+
return null;
120+
}
121+
const callsInThread = parsedTrace.Renderer.processes.get(event.pid)?.threads.get(event.tid)?.profileCalls;
122+
if (!callsInThread) {
123+
return null;
124+
}
125+
const matchByName = callsInThread.filter(e => {
126+
return e.callFrame.functionName === eventCallPoint.functionName;
127+
});
128+
129+
const lastCallBeforeEventIndex =
130+
Platform.ArrayUtilities.nearestIndexFromEnd(matchByName, profileCall => profileCall.ts <= eventCallTime);
131+
const firstCallAfterEventIndex =
132+
Platform.ArrayUtilities.nearestIndexFromBeginning(matchByName, profileCall => profileCall.ts >= eventCallTime);
133+
const lastCallBeforeEvent = typeof lastCallBeforeEventIndex === 'number' && matchByName.at(lastCallBeforeEventIndex);
134+
const firstCallAfterEvent = typeof firstCallAfterEventIndex === 'number' && matchByName.at(firstCallAfterEventIndex);
135+
136+
let closestMatchingProfileCall: Types.Events.SyntheticProfileCall;
137+
if (!lastCallBeforeEvent && !firstCallAfterEvent) {
138+
return null;
139+
}
140+
if (!lastCallBeforeEvent) {
141+
// Per the check above firstCallAfterEvent is guaranteed to exist
142+
// but ts is unaware, so we cast the type.
143+
closestMatchingProfileCall = firstCallAfterEvent as Types.Events.SyntheticProfileCall;
144+
} else if (!firstCallAfterEvent) {
145+
closestMatchingProfileCall = lastCallBeforeEvent;
146+
} else if (Helpers.Trace.eventContainsTimestamp(lastCallBeforeEvent, eventCallTime)) {
147+
closestMatchingProfileCall = lastCallBeforeEvent;
148+
} // pick the closest when the choice isn't clear.
149+
else if (eventCallTime - lastCallBeforeEvent.ts < firstCallAfterEvent.ts - eventCallTime) {
150+
closestMatchingProfileCall = lastCallBeforeEvent;
151+
} else {
152+
closestMatchingProfileCall = firstCallAfterEvent;
153+
}
154+
return get(closestMatchingProfileCall, parsedTrace);
155+
}
98156
/**
99157
* Determines if a function is a native JS API (like setTimeout,
100158
* requestAnimationFrame, consoleTask.run. etc.). This is useful to

front_end/models/trace/handlers/ExtensionTraceDataHandler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ export function extractExtensionEntries(timings: (Types.Events.SyntheticUserTimi
5555
const extensionSyntheticEntry = {
5656
name: timing.name,
5757
ph: Types.Events.Phase.COMPLETE,
58-
pid: Types.Events.ProcessID(0),
59-
tid: Types.Events.ThreadID(0),
58+
pid: timing.pid,
59+
tid: timing.tid,
6060
ts: timing.ts,
6161
dur: timing.dur as Types.Timing.MicroSeconds,
6262
cat: 'devtools.extension',

front_end/models/trace/helpers/Trace.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import * as Types from '../types/types.js';
1111
import {SyntheticEventsManager} from './SyntheticEvents.js';
1212
import {eventTimingsMicroSeconds} from './Timing.js';
1313

14-
type MatchedPairType<T extends Types.Events.PairableAsync> = Types.Events.SyntheticEventPair<T>;
1514
type MatchingPairableAsyncEvents = {
1615
begin: Types.Events.PairableAsyncBegin|null,
1716
end: Types.Events.PairableAsyncEnd|null,
@@ -179,8 +178,8 @@ export function getNavigationForTraceEvent(
179178
return navigations[eventNavigationIndex];
180179
}
181180

182-
export function extractId(event: Types.Events.PairableAsync|MatchedPairType<Types.Events.PairableAsync>): string|
183-
undefined {
181+
export function extractId(event: Types.Events.PairableAsync|
182+
Types.Events.SyntheticEventPair<Types.Events.PairableAsync>): string|undefined {
184183
return event.id ?? event.id2?.global ?? event.id2?.local;
185184
}
186185

@@ -288,9 +287,9 @@ export function createSortedSyntheticEvents<T extends Types.Events.PairableAsync
288287
end: Types.Events.PairableAsyncEnd | null,
289288
instant?: Types.Events.PairableAsyncInstant[],
290289
}>,
291-
syntheticEventCallback?: (syntheticEvent: MatchedPairType<T>) => void,
292-
): MatchedPairType<T>[] {
293-
const syntheticEvents: MatchedPairType<T>[] = [];
290+
syntheticEventCallback?: (syntheticEvent: Types.Events.SyntheticEventPair<T>) => void,
291+
): Types.Events.SyntheticEventPair<T>[] {
292+
const syntheticEvents: Types.Events.SyntheticEventPair<T>[] = [];
294293
for (const [id, eventsTriplet] of matchedPairs.entries()) {
295294
const beginEvent = eventsTriplet.begin;
296295
const endEvent = eventsTriplet.end;
@@ -311,7 +310,7 @@ export function createSortedSyntheticEvents<T extends Types.Events.PairableAsync
311310
beginEvent: Types.Events.PairableAsyncBegin,
312311
endEvent: Types.Events.PairableAsyncEnd|null,
313312
instantEvents?: Types.Events.PairableAsyncInstant[],
314-
}): data is MatchedPairType<T>['args']['data'] {
313+
}): data is Types.Events.SyntheticEventPair<T>['args']['data'] {
315314
const instantEventsMatch = data.instantEvents ? data.instantEvents.some(e => id === getSyntheticId(e)) : false;
316315
const endEventMatch = data.endEvent ? id === getSyntheticId(data.endEvent) : false;
317316
return Boolean(id) && (instantEventsMatch || endEventMatch);
@@ -321,8 +320,8 @@ export function createSortedSyntheticEvents<T extends Types.Events.PairableAsync
321320
}
322321
const targetEvent = endEvent || beginEvent;
323322

324-
const event = SyntheticEventsManager.registerSyntheticEvent<MatchedPairType<T>>({
325-
rawSourceEvent: beginEvent,
323+
const event = SyntheticEventsManager.registerSyntheticEvent<Types.Events.SyntheticEventPair<T>>({
324+
rawSourceEvent: triplet.beginEvent,
326325
cat: targetEvent.cat,
327326
ph: targetEvent.ph,
328327
pid: targetEvent.pid,
@@ -352,8 +351,8 @@ export function createSortedSyntheticEvents<T extends Types.Events.PairableAsync
352351
}
353352

354353
export function createMatchedSortedSyntheticEvents<T extends Types.Events.PairableAsync>(
355-
unpairedAsyncEvents: T[],
356-
syntheticEventCallback?: (syntheticEvent: MatchedPairType<T>) => void): MatchedPairType<T>[] {
354+
unpairedAsyncEvents: T[], syntheticEventCallback?: (syntheticEvent: Types.Events.SyntheticEventPair<T>) => void):
355+
Types.Events.SyntheticEventPair<T>[] {
357356
const matchedPairs = matchEvents(unpairedAsyncEvents);
358357
const syntheticEvents = createSortedSyntheticEvents<T>(matchedPairs, syntheticEventCallback);
359358
return syntheticEvents;
@@ -652,3 +651,7 @@ export function nodeIdForInvalidationEvent(event: Types.Events.InvalidationTrack
652651
null {
653652
return event.args.data.nodeId ?? null;
654653
}
654+
655+
export function eventContainsTimestamp(event: Types.Events.Event, ts: Types.Timing.MicroSeconds): boolean {
656+
return event.ts <= ts && event.ts + (event.dur || 0) >= ts;
657+
}

front_end/models/trace/types/Extensions.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import type {Args, Event, Phase, SyntheticBased} from './TraceEvents.js';
5+
import type {Args, Event, PerformanceMark, PerformanceMeasureBegin, Phase, SyntheticBased} from './TraceEvents.js';
66

77
export type ExtensionEntryType = 'track-entry'|'marker';
88

@@ -57,14 +57,15 @@ export interface ExtensionMarkerPayload extends ExtensionDataPayloadBase {
5757
/**
5858
* Synthetic events created for extension tracks.
5959
*/
60-
export interface SyntheticExtensionTrackEntry extends SyntheticBased<Phase.COMPLETE> {
60+
export interface SyntheticExtensionTrackEntry extends
61+
SyntheticBased<Phase.COMPLETE, PerformanceMeasureBegin|PerformanceMark> {
6162
args: Args&ExtensionTrackEntryPayload;
6263
}
6364

6465
/**
6566
* Synthetic events created for extension marks.
6667
*/
67-
export interface SyntheticExtensionMarker extends SyntheticBased<Phase.COMPLETE> {
68+
export interface SyntheticExtensionMarker extends SyntheticBased<Phase.COMPLETE, PerformanceMark> {
6869
args: Args&ExtensionMarkerPayload;
6970
}
7071

front_end/models/trace/types/TraceEvents.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1338,6 +1338,7 @@ export interface PerformanceMeasureBegin extends PairableUserTiming {
13381338
args: Args&{
13391339
detail?: string,
13401340
stackTrace?: CallFrame[],
1341+
callTime?: MicroSeconds,
13411342
};
13421343
ph: Phase.ASYNC_NESTABLE_START;
13431344
}
@@ -1350,6 +1351,7 @@ export interface PerformanceMark extends UserTiming {
13501351
data?: ArgsData & {
13511352
detail?: string,
13521353
stackTrace?: CallFrame[],
1354+
callTime?: MicroSeconds,
13531355
},
13541356
};
13551357
ph: Phase.INSTANT|Phase.MARK|Phase.ASYNC_NESTABLE_INSTANT;
@@ -1482,9 +1484,9 @@ export function isPipelineReporter(event: Event): event is PipelineReporter {
14821484
// because synthetic events need to be registered in order to resolve
14831485
// serialized event keys into event objects, so we ensure events are
14841486
// registered at the time they are created by the SyntheticEventsManager.
1485-
export interface SyntheticBased<Ph extends Phase = Phase> extends Event {
1487+
export interface SyntheticBased<Ph extends Phase = Phase, T extends Event = Event> extends Event {
14861488
ph: Ph;
1487-
rawSourceEvent: Event;
1489+
rawSourceEvent: T;
14881490
_tag: 'SyntheticEntryTag';
14891491
}
14901492

@@ -1495,8 +1497,8 @@ export function isSyntheticBased(event: Event): event is SyntheticBased {
14951497
// Nestable async events with a duration are made up of two distinct
14961498
// events: the begin, and the end. We need both of them to be able to
14971499
// display the right information, so we create these synthetic events.
1498-
export interface SyntheticEventPair<T extends PairableAsync = PairableAsync> extends SyntheticBased {
1499-
rawSourceEvent: Event;
1500+
export interface SyntheticEventPair<T extends PairableAsync = PairableAsync> extends SyntheticBased<Phase, T> {
1501+
rawSourceEvent: T;
15001502
name: T['name'];
15011503
cat: T['cat'];
15021504
id?: string;
@@ -2221,6 +2223,10 @@ export function isPerformanceMeasure(event: Event): event is PerformanceMeasure
22212223
return isUserTiming(event) && isPhaseAsync(event.ph);
22222224
}
22232225

2226+
export function isPerformanceMeasureBegin(event: Event): event is PerformanceMeasureBegin {
2227+
return isPerformanceMeasure(event) && event.ph === Phase.ASYNC_NESTABLE_START;
2228+
}
2229+
22242230
export function isPerformanceMark(event: Event): event is PerformanceMark {
22252231
return isUserTiming(event) && (event.ph === Phase.MARK || event.ph === Phase.INSTANT);
22262232
}

0 commit comments

Comments
 (0)