UNPKG

chrome-devtools-frontend

Version:
357 lines (328 loc) • 15.5 kB
// 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); }); }); });