chrome-devtools-frontend
Version:
Chrome DevTools UI
357 lines (328 loc) • 15.5 kB
text/typescript
// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import type * as CPUProfile from '../../../models/cpu_profile/cpu_profile.js';
import {describeWithEnvironment} from '../../../testing/EnvironmentHelpers.js';
import {getAllNodes, getMainThread} from '../../../testing/TraceHelpers.js';
import {TraceLoader} from '../../../testing/TraceLoader.js';
import * as Trace from '../trace.js';
async function handleEventsFromTraceFile(context: Mocha.Context|Mocha.Suite|null, name: string):
Promise<Trace.Handlers.ModelHandlers.Samples.SamplesHandlerData> {
const traceEvents = await TraceLoader.rawEvents(context, name);
Trace.Handlers.ModelHandlers.Meta.reset();
Trace.Handlers.ModelHandlers.Samples.reset();
for (const event of traceEvents) {
Trace.Handlers.ModelHandlers.Meta.handleEvent(event);
Trace.Handlers.ModelHandlers.Samples.handleEvent(event);
}
await Trace.Handlers.ModelHandlers.Meta.finalize();
await Trace.Handlers.ModelHandlers.Samples.finalize();
return Trace.Handlers.ModelHandlers.Samples.data();
}
async function handleEventsFromCpuProfile(context: Mocha.Context|Mocha.Suite|null, name: string):
Promise<Trace.Handlers.ModelHandlers.Samples.SamplesHandlerData> {
const profile = await TraceLoader.rawCPUProfile(context, name);
const contents = Trace.Helpers.SamplesIntegrator.SamplesIntegrator.createFakeTraceFromCpuProfile(
profile, Trace.Types.Events.ThreadID(1));
Trace.Handlers.ModelHandlers.Samples.reset();
for (const event of contents.traceEvents) {
Trace.Handlers.ModelHandlers.Meta.handleEvent(event);
Trace.Handlers.ModelHandlers.Samples.handleEvent(event);
}
await Trace.Handlers.ModelHandlers.Meta.finalize();
await Trace.Handlers.ModelHandlers.Samples.finalize({isCPUProfile: true});
return Trace.Handlers.ModelHandlers.Samples.data();
}
describeWithEnvironment('SamplesHandler', function() {
it('finds all the profiles in a real world recording', async () => {
const data = await handleEventsFromTraceFile(this, 'multiple-navigations-with-iframes.json.gz');
// The same thread id is shared across profiles in the profiled
// processes.
const threadId = Trace.Types.Events.ThreadID(1);
const firstProcessId = Trace.Types.Events.ProcessID(2236123);
const secondProcessId = Trace.Types.Events.ProcessID(2154214);
const thirdProcessId = Trace.Types.Events.ProcessID(2236084);
assert.strictEqual(data.profilesInProcess.size, 3);
const profilesFirstProcess = data.profilesInProcess.get(firstProcessId);
assert.strictEqual(profilesFirstProcess?.size, 1);
assert.exists(profilesFirstProcess?.get(threadId));
const profilesSecondProcess = data.profilesInProcess.get(secondProcessId);
assert.strictEqual(profilesSecondProcess?.size, 1);
assert.exists(profilesSecondProcess?.get(threadId));
const profilesThirdProcess = data.profilesInProcess.get(thirdProcessId);
assert.strictEqual(profilesThirdProcess?.size, 1);
assert.exists(profilesThirdProcess?.get(threadId));
});
describe('profile calls building', () => {
const pid = Trace.Types.Events.ProcessID(0);
const id = Trace.Types.Events.ProfileID('0');
const tid = Trace.Types.Events.ThreadID(1);
function makeProfileChunkEvent(
nodes: Array<{
id: number,
children: number[],
codeType?: string,
url?: string,
functionName?: string,
scriptId?: number,
}>,
samples: number[],
timeDeltas: number[],
ts: number,
): Trace.Types.Events.ProfileChunk {
return {
cat: '',
name: 'ProfileChunk',
ph: Trace.Types.Events.Phase.SAMPLE,
pid,
tid: Trace.Types.Events.ThreadID(0),
ts: Trace.Types.Timing.Micro(ts),
id,
args: {
data: {
cpuProfile: {
samples: samples.map(Trace.Types.Events.CallFrameID),
nodes: nodes.map(
node => ({
...node,
callFrame: {functionName: '', scriptId: 0, columnNumber: 0, lineNumber: 0, url: ''},
id: Trace.Types.Events.CallFrameID(node.id),
}),
),
},
timeDeltas: timeDeltas.map(Trace.Types.Timing.Micro),
},
},
};
}
it('can build profile calls from a CPU profile coming from tracing', async () => {
const A = 0;
const B = 1;
const C = 2;
const D = 3;
const E = 4;
const root = 9;
const mockProfileEvent: Trace.Types.Events.Profile = {
name: 'Profile',
id,
args: {data: {startTime: Trace.Types.Timing.Micro(0)}},
cat: '',
pid,
tid,
ts: Trace.Types.Timing.Micro(0),
ph: Trace.Types.Events.Phase.SAMPLE,
};
/**
* +------------> (sample at time)
* |A|A|A|A|A|A|A|A|A|A|A|A|A|A|A|A|A| |E|E|E|E|E|E|
* | |B|B|B|B|B|B| |D|D|D|D|D|D| | | | | | | | | | |
* | | |C|C|C|C| | | | | | | | | | | | | | | | | | |
* |
* V (stack trace depth)
*/
const mockTimeDeltas = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 20, 21, 22, 23,
];
const mockSamples = [A, B, C, C, C, C, B, A, D, D, D, D, D, D, A, A, A, E, E, E, E, E, E];
/*
* A E
* / \
* B D
* |
* C
*/
const mockChunks = [
makeProfileChunkEvent([{id: root, children: [A, E]}], [], [], 0),
makeProfileChunkEvent(
[{id: A, children: [B, D]}, {id: B, children: [C]}, {id: C, children: []}], mockSamples, mockTimeDeltas, 0),
makeProfileChunkEvent([{id: D, children: []}], [], [], 0),
makeProfileChunkEvent([{id: E, children: []}], [], [], 0),
];
Trace.Handlers.ModelHandlers.Samples.reset();
for (const event of [mockProfileEvent, ...mockChunks]) {
Trace.Handlers.ModelHandlers.Samples.handleEvent(event);
}
await Trace.Handlers.ModelHandlers.Samples.finalize({isCPUProfile: true});
const data = Trace.Handlers.ModelHandlers.Samples.data();
const calls = data.profilesInProcess.get(pid)?.get(tid)?.profileCalls.map(call => {
const selfTime = data.entryToNode.get(call)?.selfTime;
return {...call, selfTime};
});
const tree = data.profilesInProcess.get(pid)?.get(tid)?.profileTree;
const expectedResult = [
{id: A, ts: 0, dur: 154, selfTime: 58, children: [B, D]},
{id: B, ts: 1, dur: 27, selfTime: 9, children: [C]},
{id: C, ts: 3, dur: 18, selfTime: 18, children: []},
{id: D, ts: 36, dur: 69, selfTime: 69, children: []},
{id: E, ts: 154, dur: 117, selfTime: 117, children: []},
];
assert.exists(tree?.roots);
if (!tree?.roots) {
// This shouldn't happen, but add this if check to pass ts check.
return;
}
const allNodes = getAllNodes(tree?.roots);
const callsTestData = calls?.map(
c => {
const node = allNodes.find(node => node.id === c.nodeId);
const children = node?.children || [];
return ({
id: c.nodeId,
dur: Math.round(c.dur || 0),
ts: c.ts,
selfTime: Math.round(c.selfTime || 0),
children: [...children].map(child => child.id) || [],
});
},
);
assert.deepEqual(callsTestData, expectedResult);
});
it('can build profile calls from a CPU profile coming from a real world cpuprofile', async () => {
const data = await handleEventsFromCpuProfile(this, 'basic.cpuprofile.gz');
const threadId = Trace.Types.Events.ThreadID(1);
const firstProcessId = Trace.Types.Events.ProcessID(1);
const profilesFirstProcess = data.profilesInProcess.get(firstProcessId);
const profileData = profilesFirstProcess?.get(threadId) as Trace.Handlers.ModelHandlers.Samples.ProfileData;
// These particular calls are selected as some have children and others have selfTime
const calls = [
...profileData?.profileCalls.slice(0, 4),
...profileData?.profileCalls.slice(10, 15),
].map(call => {
const selfTime = data.entryToNode.get(call)?.selfTime;
return {...call, selfTime};
});
const tree = profileData.profileTree;
const expectedResult = [
// The initial call stack
{id: 2, dur: 2369962, ts: 73029010084, selfTime: 0, children: [3]},
{id: 3, dur: 2369962, ts: 73029010084, selfTime: 0, children: [4]},
{id: 4, dur: 2369962, ts: 73029010084, selfTime: 0, children: [5]},
{id: 5, dur: 2369962, ts: 73029010084, selfTime: 0, children: [6]},
// various calls to hrtime
{id: 10, dur: 375, ts: 73029011751, selfTime: 375, children: []},
{id: 10, dur: 1083, ts: 73029012251, selfTime: 1083, children: []},
{id: 10, dur: 833, ts: 73029013459, selfTime: 833, children: []},
{id: 10, dur: 917, ts: 73029014417, selfTime: 792, children: []},
{id: 11, dur: 125, ts: 73029014667, selfTime: 125, children: []},
];
assert.exists(tree?.roots);
if (!tree?.roots) {
// This shouldn't happen, but add this if check to pass ts check.
return;
}
const allNodes = getAllNodes(tree?.roots);
const callsTestData = calls?.map(c => {
const node = allNodes.find(node => node.id === c.nodeId);
const children = node?.children || [];
return {
id: c.nodeId,
dur: Math.round(c.dur || 0),
ts: c.ts,
selfTime: Math.round(c.selfTime || 0),
children: [...children].map(child => child.id) || [],
};
});
assert.deepEqual(callsTestData, expectedResult);
});
});
describe('CPU Profile building', () => {
it('generates a CPU profile from a trace file', async () => {
const data = await handleEventsFromTraceFile(this, 'recursive-blocking-js.json.gz');
assert.strictEqual(data.profilesInProcess.size, 1);
const profileById = data.profilesInProcess.values().next().value!;
assert.strictEqual(profileById.size, 1);
const cpuProfileData = profileById.values().next().value as Trace.Handlers.ModelHandlers.Samples.ProfileData;
const cpuProfile = cpuProfileData.rawProfile;
assert.deepEqual(
Object.keys(cpuProfile), ['startTime', 'endTime', 'nodes', 'samples', 'timeDeltas', 'lines', 'traceIds']);
assert.lengthOf(cpuProfile.nodes, 153);
assert.strictEqual(cpuProfile.startTime, 287510826176);
assert.strictEqual(cpuProfile.endTime, 287510847633);
assert.strictEqual(cpuProfile.samples?.length, 39471);
assert.strictEqual(cpuProfile.samples?.length, cpuProfile.timeDeltas?.length);
assert.strictEqual(cpuProfile.samples?.length, cpuProfile.lines?.length);
});
});
describe('CPU Profile parsing', () => {
it('generates a parsed CPU profile from a trace file', async () => {
const data = await handleEventsFromTraceFile(this, 'recursive-blocking-js.json.gz');
assert.strictEqual(data.profilesInProcess.size, 1);
const profileById = data.profilesInProcess.values().next().value!;
assert.strictEqual(profileById.size, 1);
const cpuProfileData = profileById.values().next().value as Trace.Handlers.ModelHandlers.Samples.ProfileData;
const parsedProfile = cpuProfileData.parsedProfile;
assert.strictEqual(parsedProfile.nodes()?.length, 153);
// Ensure that we correctly maintain a lineNumber/columnNumber of 0 and don't fall back to -1 because 0 is falsey.
const nodesWithZeroLineNumber = parsedProfile.nodes()?.filter(node => node.lineNumber === 0) || [];
assert.lengthOf(nodesWithZeroLineNumber, 15);
const nodesWithZeroColumnNumber = parsedProfile.nodes()?.filter(node => node.columnNumber === 0) || [];
assert.lengthOf(nodesWithZeroColumnNumber, 12);
assert.strictEqual(parsedProfile.gcNode?.id, 36);
assert.strictEqual(parsedProfile.programNode?.id, 2);
assert.strictEqual(parsedProfile.profileStartTime, 287510835.138);
assert.strictEqual(parsedProfile.profileEndTime, 287515908.9025441);
assert.strictEqual(parsedProfile.maxDepth, 14);
assert.strictEqual(parsedProfile.samples?.length, 39471);
});
});
describe('getProfileCallFunctionName', () => {
// Find an event from the trace that represents some work. The use of
// this specific call frame event is not for any real reason.
function getProfileEventAndNode(parsedTrace: Trace.Handlers.Types.ParsedTrace): {
entry: Trace.Types.Events.SyntheticProfileCall,
profileNode: CPUProfile.ProfileTreeModel.ProfileNode,
} {
const mainThread = getMainThread(parsedTrace.Renderer);
let foundNode: CPUProfile.ProfileTreeModel.ProfileNode|null = null;
let foundEntry: Trace.Types.Events.SyntheticProfileCall|null = null;
for (const entry of mainThread.entries) {
if (Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === 'performConcurrentWorkOnRoot') {
const profile = parsedTrace.Samples.profilesInProcess.get(entry.pid)?.get(entry.tid);
const node = profile?.parsedProfile.nodeById(entry.nodeId);
if (node) {
foundNode = node;
}
foundEntry = entry;
break;
}
}
if (!foundNode) {
throw new Error('Could not find CPU Profile node.');
}
if (!foundEntry) {
throw new Error('Could not find expected entry.');
}
return {
entry: foundEntry,
profileNode: foundNode,
};
}
it('falls back to the call frame name if the ProfileNode name is empty', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'react-hello-world.json.gz');
const {entry, profileNode} = getProfileEventAndNode(parsedTrace);
// Store and then reset this: we are doing this to test the fallback to
// the entry callFrame.functionName property. After the assertion we
// reset this to avoid impacting other tests.
const originalProfileNodeName = profileNode.functionName;
profileNode.setFunctionName('');
assert.strictEqual(
Trace.Handlers.ModelHandlers.Samples.getProfileCallFunctionName(parsedTrace.Samples, entry),
'performConcurrentWorkOnRoot');
// St
profileNode.setFunctionName(originalProfileNodeName);
});
it('uses the profile name if it has been set', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'react-hello-world.json.gz');
const {entry, profileNode} = getProfileEventAndNode(parsedTrace);
// Store and then reset this: we are doing this to test the fallback to
// the entry callFrame.functionName property. After the assertion we
// reset this to avoid impacting other tests.
const originalProfileNodeName = profileNode.functionName;
profileNode.setFunctionName('testing-profile-name');
assert.strictEqual(
Trace.Handlers.ModelHandlers.Samples.getProfileCallFunctionName(parsedTrace.Samples, entry),
'testing-profile-name');
profileNode.setFunctionName(originalProfileNodeName);
});
});
});