Skip to content

Commit bb10e69

Browse files
paulirishDevtools-frontend LUCI CQ
authored andcommitted
RPP: Skip buildProfileCalls in SamplesHandler if it's an actual trace
Also clean up some trace vs cpuprofile tech debt Bug:40278532 Change-Id: I1020fd61d1ff17f1cb24bbb92e94d066a25c0b75 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6005888 Auto-Submit: Paul Irish <[email protected]> Reviewed-by: Andres Olivares <[email protected]> Commit-Queue: Paul Irish <[email protected]>
1 parent 4721900 commit bb10e69

File tree

12 files changed

+169
-138
lines changed

12 files changed

+169
-138
lines changed

front_end/models/trace/Processor.ts

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -49,21 +49,6 @@ declare global {
4949
}
5050
}
5151

52-
export interface ParseOptions {
53-
/**
54-
* If the trace was just recorded on the current page, rather than an imported file.
55-
* TODO(paulirish): Maybe remove. This is currently unused by the Processor and Handlers
56-
* @default false
57-
*/
58-
isFreshRecording?: boolean;
59-
/**
60-
* If the trace is a CPU Profile rather than a Chrome tracing trace.
61-
* @default false
62-
*/
63-
isCPUProfile?: boolean;
64-
metadata?: Types.File.MetaData;
65-
}
66-
6752
export class TraceProcessor extends EventTarget {
6853
// We force the Meta handler to be enabled, so the TraceHandlers type here is
6954
// the model handlers the user passes in and the Meta handler.
@@ -166,13 +151,13 @@ export class TraceProcessor extends EventTarget {
166151
this.#status = Status.IDLE;
167152
}
168153

169-
async parse(traceEvents: readonly Types.Events.Event[], options: ParseOptions): Promise<void> {
154+
async parse(traceEvents: readonly Types.Events.Event[], options: Types.Configuration.ParseOptions): Promise<void> {
170155
if (this.#status !== Status.IDLE) {
171156
throw new Error(`Trace processor can't start parsing when not idle. Current state: ${this.#status}`);
172157
}
173158
try {
174159
this.#status = Status.PARSING;
175-
await this.#computeParsedTrace(traceEvents);
160+
await this.#computeParsedTrace(traceEvents, options);
176161
if (this.#data && !options.isCPUProfile) { // We do not calculate insights for CPU Profiles.
177162
this.#computeInsights(this.#data, traceEvents, options);
178163
}
@@ -186,7 +171,8 @@ export class TraceProcessor extends EventTarget {
186171
/**
187172
* Run all the handlers and set the result to `#data`.
188173
*/
189-
async #computeParsedTrace(traceEvents: readonly Types.Events.Event[]): Promise<void> {
174+
async #computeParsedTrace(traceEvents: readonly Types.Events.Event[], options: Types.Configuration.ParseOptions):
175+
Promise<void> {
190176
/**
191177
* We want to yield regularly to maintain responsiveness. If we yield too often, we're wasting idle time.
192178
* We could do this by checking `performance.now()` regularly, but it's an expensive call in such a hot loop.
@@ -226,7 +212,7 @@ export class TraceProcessor extends EventTarget {
226212
// Yield to the UI because finalize() calls can be expensive
227213
// TODO(jacktfranklin): consider using `scheduler.yield()` or `scheduler.postTask(() => {}, {priority: 'user-blocking'})`
228214
await new Promise(resolve => setTimeout(resolve, 0));
229-
await handler.finalize();
215+
await handler.finalize(options);
230216
}
231217
const percent = calculateProgress(i / sortedHandlers.length, ProgressPhase.FINALIZE);
232218
this.dispatchEvent(new TraceParseProgressEvent({percent}));
@@ -442,7 +428,7 @@ export class TraceProcessor extends EventTarget {
442428
#computeInsightSet(
443429
insights: Insights.Types.TraceInsightSets, parsedTrace: Handlers.Types.ParsedTrace,
444430
insightRunners: Partial<typeof Insights.Models>, context: Insights.Types.InsightSetContext,
445-
options: ParseOptions): void {
431+
options: Types.Configuration.ParseOptions): void {
446432
const model = {} as Insights.Types.InsightSet['model'];
447433

448434
for (const [name, insight] of Object.entries(insightRunners)) {
@@ -491,7 +477,7 @@ export class TraceProcessor extends EventTarget {
491477
*/
492478
#computeInsights(
493479
parsedTrace: Handlers.Types.ParsedTrace, traceEvents: readonly Types.Events.Event[],
494-
options: ParseOptions): void {
480+
options: Types.Configuration.ParseOptions): void {
495481
this.#insights = new Map();
496482

497483
const enabledInsightRunners = TraceProcessor.getEnabledInsightRunners(parsedTrace);

front_end/models/trace/extras/TimelineJSProfile.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,14 @@ export class TimelineJSProfileProcessor {
2626
}
2727

2828
static createFakeTraceFromCpuProfile(profile: Protocol.Profiler.Profile, tid: Types.Events.ThreadID):
29-
Types.Events.Event[] {
29+
Types.File.TraceFile {
3030
const events: Types.Events.Event[] = [];
3131

3232
const threadName = `Thread ${tid}`;
3333
appendEvent('TracingStartedInPage', {data: {sessionId: '1'}}, 0, 0, Types.Events.Phase.METADATA);
3434
appendEvent(Types.Events.Name.THREAD_NAME, {name: threadName}, 0, 0, Types.Events.Phase.METADATA, '__metadata');
3535
if (!profile) {
36-
return events;
36+
return {traceEvents: events, metadata: {}};
3737
}
3838

3939
// Append a root to show the start time of the profile (which is earlier than first sample), so the Performance
@@ -45,7 +45,12 @@ export class TimelineJSProfileProcessor {
4545

4646
// TODO: create a `Profile` event instead, as `cpuProfile` is legacy
4747
appendEvent('CpuProfile', {data: {cpuProfile: profile}}, profile.endTime, 0, Types.Events.Phase.COMPLETE);
48-
return events;
48+
return {
49+
traceEvents: events,
50+
metadata: {
51+
dataOrigin: Types.File.DataOrigin.CPU_PROFILE,
52+
}
53+
};
4954

5055
function appendEvent(
5156
name: string, args: any, ts: number, dur?: number, ph?: Types.Events.Phase, cat?: string): Types.Events.Event {

front_end/models/trace/handlers/SamplesHandler.test.ts

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,25 @@ async function handleEventsFromTraceFile(context: Mocha.Context|Mocha.Suite|null
2525
return Trace.Handlers.ModelHandlers.Samples.data();
2626
}
2727

28+
async function handleEventsFromCpuProfile(context: Mocha.Context|Mocha.Suite|null, name: string):
29+
Promise<Trace.Handlers.ModelHandlers.Samples.SamplesHandlerData> {
30+
const profile = await TraceLoader.rawCPUProfile(context, name);
31+
32+
const contents = Trace.Extras.TimelineJSProfile.TimelineJSProfileProcessor.createFakeTraceFromCpuProfile(
33+
profile, Trace.Types.Events.ThreadID(1));
34+
35+
Trace.Handlers.ModelHandlers.Samples.reset();
36+
for (const event of contents.traceEvents) {
37+
Trace.Handlers.ModelHandlers.Meta.handleEvent(event);
38+
Trace.Handlers.ModelHandlers.Samples.handleEvent(event);
39+
}
40+
41+
await Trace.Handlers.ModelHandlers.Meta.finalize();
42+
await Trace.Handlers.ModelHandlers.Samples.finalize({isCPUProfile: true});
43+
44+
return Trace.Handlers.ModelHandlers.Samples.data();
45+
}
46+
2847
describeWithEnvironment('SamplesHandler', function() {
2948
it('finds all the profiles in a real world recording', async () => {
3049
const data = await handleEventsFromTraceFile(this, 'multiple-navigations-with-iframes.json.gz');
@@ -141,7 +160,7 @@ describeWithEnvironment('SamplesHandler', function() {
141160
for (const event of [mockProfileEvent, ...mockChunks]) {
142161
Trace.Handlers.ModelHandlers.Samples.handleEvent(event);
143162
}
144-
await Trace.Handlers.ModelHandlers.Samples.finalize();
163+
await Trace.Handlers.ModelHandlers.Samples.finalize({isCPUProfile: true});
145164
const data = Trace.Handlers.ModelHandlers.Samples.data();
146165
const calls = data.profilesInProcess.get(pid)?.get(tid)?.profileCalls.map(call => {
147166
const selfTime = data.entryToNode.get(call)?.selfTime;
@@ -177,23 +196,36 @@ describeWithEnvironment('SamplesHandler', function() {
177196

178197
assert.deepEqual(callsTestData, expectedResult);
179198
});
180-
it('can build profile calls from a CPU profile coming from a real world trace', async () => {
181-
const data = await handleEventsFromTraceFile(this, 'multiple-navigations-with-iframes.json.gz');
199+
200+
it('can build profile calls from a CPU profile coming from a real world cpuprofile', async () => {
201+
const data = await handleEventsFromCpuProfile(this, 'basic.cpuprofile.gz');
182202

183203
const threadId = Trace.Types.Events.ThreadID(1);
184-
const firstProcessId = Trace.Types.Events.ProcessID(2236123);
204+
const firstProcessId = Trace.Types.Events.ProcessID(1);
185205
const profilesFirstProcess = data.profilesInProcess.get(firstProcessId);
186-
const calls = profilesFirstProcess?.get(threadId)?.profileCalls.slice(0, 5).map(call => {
206+
const profileData = profilesFirstProcess?.get(threadId) as Trace.Handlers.ModelHandlers.Samples.ProfileData;
207+
// These particular calls are selected as some have children and others have selfTime
208+
const calls = [
209+
...profileData?.profileCalls.slice(0, 4),
210+
...profileData?.profileCalls.slice(10, 15),
211+
].map(call => {
187212
const selfTime = data.entryToNode.get(call)?.selfTime;
188213
return {...call, selfTime};
189214
});
190-
const tree = profilesFirstProcess?.get(threadId)?.profileTree;
215+
const tree = profileData.profileTree;
191216
const expectedResult = [
192-
{id: 2, dur: 392, ts: 643496962681, selfTime: 392, children: []},
193-
{id: 3, dur: 682, ts: 643496963073, selfTime: 0, children: [4]},
194-
{id: 4, dur: 682, ts: 643496963073, selfTime: 160, children: [5]},
195-
{id: 5, dur: 522, ts: 643496963233, selfTime: 178, children: [6, 7]},
196-
{id: 6, dur: 175, ts: 643496963411, selfTime: 175, children: []},
217+
// The initial call stack
218+
{id: 2, dur: 2369962, ts: 73029010084, selfTime: 0, children: [3]},
219+
{id: 3, dur: 2369962, ts: 73029010084, selfTime: 0, children: [4]},
220+
{id: 4, dur: 2369962, ts: 73029010084, selfTime: 0, children: [5]},
221+
{id: 5, dur: 2369962, ts: 73029010084, selfTime: 0, children: [6]},
222+
// various calls to hrtime
223+
{id: 10, dur: 375, ts: 73029011751, selfTime: 375, children: []},
224+
{id: 10, dur: 1083, ts: 73029012251, selfTime: 1083, children: []},
225+
{id: 10, dur: 833, ts: 73029013459, selfTime: 833, children: []},
226+
{id: 10, dur: 917, ts: 73029014417, selfTime: 792, children: []},
227+
{id: 11, dur: 125, ts: 73029014667, selfTime: 125, children: []},
228+
197229
];
198230
assert.exists(tree?.roots);
199231
if (!tree?.roots) {

front_end/models/trace/handlers/SamplesHandler.ts

Lines changed: 55 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const entryToNode = new Map<Types.Events.Event, Helpers.TreeHelpers.TraceEntryNo
2424
// events matched by thread id.
2525
const preprocessedData = new Map<Types.Events.ProcessID, Map<Types.Events.ProfileID, PreprocessedData>>();
2626

27-
function buildProfileCalls(): void {
27+
function parseCPUProfileData(parseOptions: Types.Configuration.ParseOptions): void {
2828
for (const [processId, profiles] of preprocessedData) {
2929
for (const [profileId, preProcessedData] of profiles) {
3030
const threadId = preProcessedData.threadId;
@@ -44,58 +44,65 @@ function buildProfileCalls(): void {
4444
profileTree,
4545
profileId,
4646
};
47-
4847
const dataByThread = Platform.MapUtilities.getWithDefault(profilesInProcess, processId, () => new Map());
49-
profileModel.forEachFrame(openFrameCallback, closeFrameCallback);
5048
dataByThread.set(threadId, finalizedData);
5149

52-
function openFrameCallback(
53-
depth: number, node: CPUProfile.ProfileTreeModel.ProfileNode, sampleIndex: number,
54-
timeStampMilliseconds: number): void {
55-
if (threadId === undefined) {
56-
return;
57-
}
58-
const ts = Helpers.Timing.milliToMicro(Types.Timing.Milli(timeStampMilliseconds));
59-
const nodeId = node.id as Helpers.TreeHelpers.TraceEntryNodeId;
60-
61-
const profileCall = Helpers.Trace.makeProfileCall(node, profileId, sampleIndex, ts, processId, threadId);
62-
finalizedData.profileCalls.push(profileCall);
63-
indexStack.push(finalizedData.profileCalls.length - 1);
64-
const traceEntryNode = Helpers.TreeHelpers.makeEmptyTraceEntryNode(profileCall, nodeId);
65-
entryToNode.set(profileCall, traceEntryNode);
66-
traceEntryNode.depth = depth;
67-
if (indexStack.length === 1) {
68-
// First call in the stack is a root call.
69-
finalizedData.profileTree?.roots.add(traceEntryNode);
70-
}
50+
// Only need to build pure JS ProfileCalls if we're parsing a CPU Profile, otherwise SamplesIntegrator does the work.
51+
if (parseOptions.isCPUProfile) {
52+
buildProfileCallsForCPUProfile();
7153
}
72-
function closeFrameCallback(
73-
_depth: number, _node: CPUProfile.ProfileTreeModel.ProfileNode, _sampleIndex: number,
74-
_timeStampMillis: number, durMs: number, selfTimeMs: number): void {
75-
const profileCallIndex = indexStack.pop();
76-
const profileCall = profileCallIndex !== undefined && finalizedData.profileCalls[profileCallIndex];
77-
if (!profileCall) {
78-
return;
79-
}
80-
const {callFrame, ts, pid, tid} = profileCall;
81-
const traceEntryNode = entryToNode.get(profileCall);
82-
if (callFrame === undefined || ts === undefined || pid === undefined || profileId === undefined ||
83-
tid === undefined || traceEntryNode === undefined) {
84-
return;
54+
55+
function buildProfileCallsForCPUProfile(): void {
56+
profileModel.forEachFrame(openFrameCallback, closeFrameCallback);
57+
58+
function openFrameCallback(
59+
depth: number, node: CPUProfile.ProfileTreeModel.ProfileNode, sampleIndex: number,
60+
timeStampMilliseconds: number): void {
61+
if (threadId === undefined) {
62+
return;
63+
}
64+
const ts = Helpers.Timing.milliToMicro(Types.Timing.Milli(timeStampMilliseconds));
65+
const nodeId = node.id as Helpers.TreeHelpers.TraceEntryNodeId;
66+
67+
const profileCall = Helpers.Trace.makeProfileCall(node, profileId, sampleIndex, ts, processId, threadId);
68+
finalizedData.profileCalls.push(profileCall);
69+
indexStack.push(finalizedData.profileCalls.length - 1);
70+
const traceEntryNode = Helpers.TreeHelpers.makeEmptyTraceEntryNode(profileCall, nodeId);
71+
entryToNode.set(profileCall, traceEntryNode);
72+
traceEntryNode.depth = depth;
73+
if (indexStack.length === 1) {
74+
// First call in the stack is a root call.
75+
finalizedData.profileTree?.roots.add(traceEntryNode);
76+
}
8577
}
86-
const dur = Helpers.Timing.milliToMicro(Types.Timing.Milli(durMs));
87-
const selfTime = Helpers.Timing.milliToMicro(Types.Timing.Milli(selfTimeMs));
88-
profileCall.dur = dur;
89-
traceEntryNode.selfTime = selfTime;
78+
function closeFrameCallback(
79+
_depth: number, _node: CPUProfile.ProfileTreeModel.ProfileNode, _sampleIndex: number,
80+
_timeStampMillis: number, durMs: number, selfTimeMs: number): void {
81+
const profileCallIndex = indexStack.pop();
82+
const profileCall = profileCallIndex !== undefined && finalizedData.profileCalls[profileCallIndex];
83+
if (!profileCall) {
84+
return;
85+
}
86+
const {callFrame, ts, pid, tid} = profileCall;
87+
const traceEntryNode = entryToNode.get(profileCall);
88+
if (callFrame === undefined || ts === undefined || pid === undefined || profileId === undefined ||
89+
tid === undefined || traceEntryNode === undefined) {
90+
return;
91+
}
92+
const dur = Helpers.Timing.milliToMicro(Types.Timing.Milli(durMs));
93+
const selfTime = Helpers.Timing.milliToMicro(Types.Timing.Milli(selfTimeMs));
94+
profileCall.dur = dur;
95+
traceEntryNode.selfTime = selfTime;
9096

91-
const parentIndex = indexStack.at(-1);
92-
const parent = parentIndex !== undefined && finalizedData.profileCalls.at(parentIndex);
93-
const parentNode = parent && entryToNode.get(parent);
94-
if (!parentNode) {
95-
return;
97+
const parentIndex = indexStack.at(-1);
98+
const parent = parentIndex !== undefined && finalizedData.profileCalls.at(parentIndex);
99+
const parentNode = parent && entryToNode.get(parent);
100+
if (!parentNode) {
101+
return;
102+
}
103+
traceEntryNode.parent = parentNode;
104+
parentNode.children.push(traceEntryNode);
96105
}
97-
traceEntryNode.parent = parentNode;
98-
parentNode.children.push(traceEntryNode);
99106
}
100107
}
101108
}
@@ -183,8 +190,8 @@ export function handleEvent(event: Types.Events.Event): void {
183190
}
184191
}
185192

186-
export async function finalize(): Promise<void> {
187-
buildProfileCalls();
193+
export async function finalize(parseOptions: Types.Configuration.ParseOptions = {}): Promise<void> {
194+
parseCPUProfileData(parseOptions);
188195
}
189196

190197
export function data(): SamplesHandlerData {

front_end/models/trace/handlers/Threads.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@ describeWithEnvironment('Handler Threads helper', function() {
3838
// Bit of extra setup required: we need to mimic what the panel does where
3939
// it takes the CDP Profile and wraps it in fake trace events, before then
4040
// passing that through to the new engine.
41-
const rawEvents = await TraceLoader.rawCPUProfile(this, 'node-fibonacci-website.cpuprofile.gz');
42-
const events = Trace.Extras.TimelineJSProfile.TimelineJSProfileProcessor.createFakeTraceFromCpuProfile(
43-
rawEvents,
41+
const profile = await TraceLoader.rawCPUProfile(this, 'node-fibonacci-website.cpuprofile.gz');
42+
const contents = Trace.Extras.TimelineJSProfile.TimelineJSProfileProcessor.createFakeTraceFromCpuProfile(
43+
profile,
4444
Trace.Types.Events.ThreadID(1),
4545
);
46-
const {parsedTrace} =
47-
await TraceLoader.executeTraceEngineOnFileContents(events as unknown as Trace.Types.Events.Event[]);
46+
47+
const {parsedTrace} = await TraceLoader.executeTraceEngineOnFileContents(contents);
4848

4949
// Check that we did indeed parse this properly as a CPU Profile.
5050
assert.strictEqual(parsedTrace.Renderer.processes.size, 0);

front_end/models/trace/handlers/Threads.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,16 +98,15 @@ export function threadsInTrace(parsedTrace: ParsedTrace): readonly ThreadData[]
9898
return cached;
9999
}
100100

101-
// If we have Renderer threads, we prefer to use those. In the event that a
102-
// trace is a CPU Profile trace, we will never have Renderer threads, so we
103-
// know if there are no Renderer threads that we can fallback to using the
104-
// data from the SamplesHandler.
101+
// If we have Renderer threads, we prefer to use those.
105102
const threadsFromRenderer = threadsInRenderer(parsedTrace.Renderer, parsedTrace.AuctionWorklets);
106103
if (threadsFromRenderer.length) {
107104
threadsInTraceCache.set(parsedTrace, threadsFromRenderer);
108105
return threadsFromRenderer;
109106
}
110107

108+
// If it's a CPU Profile trace, there will be no Renderer threads.
109+
// We can fallback to using the data from the SamplesHandler.
111110
const foundThreads: ThreadData[] = [];
112111
if (parsedTrace.Samples.profilesInProcess.size) {
113112
for (const [pid, process] of parsedTrace.Samples.profilesInProcess) {

front_end/models/trace/handlers/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ import type * as ModelHandlers from './ModelHandlers.js';
88
export interface Handler {
99
reset(): void;
1010
handleEvent(data: {}): void;
11-
finalize(): Promise<void>;
11+
finalize(options?: Types.Configuration.ParseOptions): Promise<void>;
1212
data(): unknown;
1313
deps?(): HandlerName[];
1414
handleUserConfig?(config: Types.Configuration.Configuration): void;
1515
}
16+
1617
export type HandlerName = keyof typeof ModelHandlers;
1718

1819
// This type maps Handler names to the return type of their data

front_end/models/trace/types/Configuration.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
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 * as File from './File.js';
6+
57
export interface Configuration {
68
/**
79
* Include V8 RCS functions in the JS stacks
@@ -48,3 +50,18 @@ export const defaults = (): Configuration => ({
4850
export function configToCacheKey(config: Configuration): string {
4951
return JSON.stringify(config);
5052
}
53+
54+
export interface ParseOptions {
55+
/**
56+
* If the trace was just recorded on the current page, rather than an imported file.
57+
* TODO(paulirish): Maybe remove. This is currently unused by the Processor and Handlers
58+
* @default false
59+
*/
60+
isFreshRecording?: boolean;
61+
/**
62+
* If the trace is a CPU Profile rather than a Chrome tracing trace.
63+
* @default false
64+
*/
65+
isCPUProfile?: boolean;
66+
metadata?: File.MetaData;
67+
}

0 commit comments

Comments
 (0)