| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import type * as Protocol from '../../generated/protocol.js'; |
| import {describeWithEnvironment} from '../../testing/EnvironmentHelpers.js'; |
| import * as CPUProfile from '../cpu_profile/cpu_profile.js'; |
| |
| function makeCallFrame(functionName: string): Protocol.Runtime.CallFrame { |
| return { |
| functionName, |
| scriptId: 'ScriptId', |
| url: '', |
| lineNumber: 0, |
| columnNumber: 0, |
| } as unknown as Protocol.Runtime.CallFrame; |
| } |
| |
| function getFrameTreeAsString(cpuProfileDataModel: CPUProfile.CPUProfileDataModel.CPUProfileDataModel): string { |
| interface Entry { |
| ts: number; |
| dur: number; |
| name: string; |
| selfTime: number; |
| id: number; |
| depth: number; |
| } |
| const trackingStack: Entry[] = []; |
| const resultStack: Entry[] = []; |
| let result = '\n'; |
| const onFrameOpen = |
| (depth: number, node: CPUProfile.ProfileTreeModel.ProfileNode, _sampleIndex: number, ts: number) => { |
| trackingStack.push({depth, id: node.id, name: node.callFrame.functionName, ts, selfTime: 0, dur: 0}); |
| }; |
| const onFrameClose = |
| (_depth: number, node: CPUProfile.ProfileTreeModel.ProfileNode, _sampleIndex: number, _ts: number, dur: number, |
| selfTime: number) => { |
| const entry = trackingStack.pop(); |
| if (!entry || entry.id !== node.id) { |
| throw new Error('Frame open and Frame close callbacks are not balanced'); |
| } |
| entry.dur = dur; |
| entry.selfTime = selfTime; |
| resultStack.push(entry); |
| }; |
| cpuProfileDataModel.forEachFrame(onFrameOpen, onFrameClose); |
| resultStack.sort((a, b) => b.ts - a.ts); |
| while (resultStack.length) { |
| const entry = resultStack.pop(); |
| if (!entry) { |
| break; |
| } |
| const {depth, name, ts, dur, selfTime} = entry; |
| result += ' '.repeat(depth) + |
| `name: ${name} ts: ${ts} dur: ${Math.round(dur * 100) / 100} selfTime: ${Math.round(selfTime * 100) / 100}`; |
| result += resultStack.length ? '\n' : ''; |
| } |
| return result; |
| } |
| |
| describe('ProfileTreeModel', function() { |
| it('calculates self and total times correctly for a CPU profile', () => { |
| // Create the following tree: |
| // |
| // root (self = 10) |
| // / \ |
| // A (self = 0) D (self = 10) |
| // / \ \ |
| // B (self = 20) C (self = 10) E (self = 20) |
| const callFrameRoot = makeCallFrame('root'); |
| const root = new CPUProfile.ProfileTreeModel.ProfileNode(callFrameRoot); |
| root.self = 10; |
| |
| const callFrameA = makeCallFrame('A'); |
| const nodeA = new CPUProfile.ProfileTreeModel.ProfileNode(callFrameA); |
| root.children.push(nodeA); |
| nodeA.self = 0; |
| |
| const callFrameB = makeCallFrame('B'); |
| const nodeB = new CPUProfile.ProfileTreeModel.ProfileNode(callFrameB); |
| nodeA.children.push(nodeB); |
| nodeB.self = 20; |
| |
| const callFrameC = makeCallFrame('C'); |
| const nodeC = new CPUProfile.ProfileTreeModel.ProfileNode(callFrameC); |
| nodeA.children.push(nodeC); |
| nodeC.self = 10; |
| |
| const callFrameD = makeCallFrame('D'); |
| const nodeD = new CPUProfile.ProfileTreeModel.ProfileNode(callFrameD); |
| root.children.push(nodeD); |
| nodeD.self = 10; |
| |
| const callFrameE = makeCallFrame('E'); |
| const nodeE = new CPUProfile.ProfileTreeModel.ProfileNode(callFrameE); |
| nodeD.children.push(nodeE); |
| nodeE.self = 20; |
| |
| const profileTreeModel = new CPUProfile.ProfileTreeModel.ProfileTreeModel(); |
| profileTreeModel.initialize(root); |
| |
| assert.strictEqual(profileTreeModel.total, 70); |
| |
| assert.strictEqual(root.total, 70); |
| assert.strictEqual(root.self, 10); |
| assert.strictEqual(nodeA.total, 30); |
| assert.strictEqual(nodeA.self, 0); |
| assert.strictEqual(nodeB.total, 20); |
| assert.strictEqual(nodeB.self, 20); |
| assert.strictEqual(nodeC.total, 10); |
| assert.strictEqual(nodeC.self, 10); |
| assert.strictEqual(nodeD.total, 30); |
| assert.strictEqual(nodeD.self, 10); |
| assert.strictEqual(nodeE.total, 20); |
| assert.strictEqual(nodeE.self, 20); |
| }); |
| it('calculates depth correctly for the nodes in a profile tree', () => { |
| // Create the following tree: |
| // |
| // root |
| // / \ |
| // A D |
| // / \ \ |
| // B C E |
| // \ |
| // F |
| const callFrameRoot = makeCallFrame('root'); |
| const root = new CPUProfile.ProfileTreeModel.ProfileNode(callFrameRoot); |
| |
| const callFrameA = makeCallFrame('A'); |
| const nodeA = new CPUProfile.ProfileTreeModel.ProfileNode(callFrameA); |
| root.children.push(nodeA); |
| |
| const callFrameB = makeCallFrame('B'); |
| const nodeB = new CPUProfile.ProfileTreeModel.ProfileNode(callFrameB); |
| nodeA.children.push(nodeB); |
| |
| const callFrameC = makeCallFrame('C'); |
| const nodeC = new CPUProfile.ProfileTreeModel.ProfileNode(callFrameC); |
| nodeA.children.push(nodeC); |
| |
| const callFrameD = makeCallFrame('D'); |
| const nodeD = new CPUProfile.ProfileTreeModel.ProfileNode(callFrameD); |
| root.children.push(nodeD); |
| |
| const callFrameE = makeCallFrame('E'); |
| const nodeE = new CPUProfile.ProfileTreeModel.ProfileNode(callFrameE); |
| nodeD.children.push(nodeE); |
| |
| const callFrameF = makeCallFrame('F'); |
| const nodeF = new CPUProfile.ProfileTreeModel.ProfileNode(callFrameF); |
| nodeE.children.push(nodeF); |
| |
| const profileTreeModel = new CPUProfile.ProfileTreeModel.ProfileTreeModel(); |
| profileTreeModel.initialize(root); |
| |
| assert.strictEqual(profileTreeModel.maxDepth, 3); |
| assert.strictEqual(root.depth, -1); |
| assert.strictEqual(nodeA.depth, 0); |
| assert.strictEqual(nodeB.depth, 1); |
| assert.strictEqual(nodeC.depth, 1); |
| assert.strictEqual(nodeD.depth, 0); |
| assert.strictEqual(nodeE.depth, 1); |
| assert.strictEqual(nodeF.depth, 2); |
| }); |
| }); |
| |
| describeWithEnvironment('CPUProfileDataModel', () => { |
| const buildBasicProfile = () => { |
| const scriptId = 'Peperoni' as Protocol.Runtime.ScriptId; |
| const url = ''; |
| const lineNumber = -1; |
| const columnNumber = -1; |
| const profile: Protocol.Profiler.Profile = { |
| startTime: 1000, |
| endTime: 3000, |
| nodes: [ |
| { |
| id: 1, |
| hitCount: 0, |
| callFrame: {functionName: '(root)', scriptId, url, lineNumber, columnNumber}, |
| children: [2, 3], |
| }, |
| {id: 2, hitCount: 3, callFrame: {functionName: 'a', scriptId, url, lineNumber, columnNumber}, children: [4, 5]}, |
| {id: 3, hitCount: 3, callFrame: {functionName: 'b', scriptId, url, lineNumber, columnNumber}, children: [6]}, |
| {id: 4, hitCount: 2, callFrame: {functionName: 'c', scriptId, url, lineNumber, columnNumber}}, |
| {id: 5, hitCount: 1, callFrame: {functionName: 'd', scriptId, url, lineNumber, columnNumber}}, |
| {id: 6, hitCount: 2, callFrame: {functionName: 'e', scriptId, url, lineNumber, columnNumber}, children: [7]}, |
| {id: 7, hitCount: 2, callFrame: {functionName: 'f', scriptId, url, lineNumber, columnNumber}}, |
| ], |
| samples: [2, 2, 4, 5, 4, 2, 3, 6, 6, 7, 7, 3, 3], |
| timeDeltas: new Array(13).fill(100), |
| }; |
| return profile; |
| }; |
| it('builds a tree from a CPU profile', () => { |
| const profile = buildBasicProfile(); |
| // Profile contains this tree: |
| // |
| // 1 |
| // / \ |
| // 2 3 |
| // / \ \ |
| // 4 5 6 |
| // \ |
| // 7 |
| const cpuProfileDataModel = new CPUProfile.CPUProfileDataModel.CPUProfileDataModel(profile); |
| assert.deepEqual(cpuProfileDataModel.root.children.map(n => n.id), [3, 2]); |
| |
| const node2 = cpuProfileDataModel.root.children[1]; |
| assert.strictEqual(node2.id, 2); |
| assert.deepEqual(node2.children.map(n => n.id), [5, 4]); |
| |
| const node3 = cpuProfileDataModel.root.children[0]; |
| assert.strictEqual(node3.id, 3); |
| assert.deepEqual(node3.children.map(n => n.id), [6]); |
| |
| const node6 = node3.children[0]; |
| assert.strictEqual(node6.id, 6); |
| assert.deepEqual(node6.children.map(n => n.id), [7]); |
| }); |
| it('parses JS call frames from a CPU profile', () => { |
| // Calls in the profile look roughly like |
| // |
| // |---------------a--------------||---------b---------| |
| // |---c---||--d--||---c---| |-------e------| |
| // |-----f-----| |
| const profile = buildBasicProfile(); |
| const cpuProfileDataModel = new CPUProfile.CPUProfileDataModel.CPUProfileDataModel(profile); |
| const treeAsString = getFrameTreeAsString(cpuProfileDataModel); |
| |
| assert.strictEqual(treeAsString, ` |
| name: a ts: 1.1 dur: 0.6 selfTime: 0.3 |
| name: c ts: 1.3 dur: 0.1 selfTime: 0.1 |
| name: d ts: 1.4 dur: 0.1 selfTime: 0.1 |
| name: c ts: 1.5 dur: 0.1 selfTime: 0.1 |
| name: b ts: 1.7 dur: 0.7 selfTime: 0.3 |
| name: e ts: 1.8 dur: 0.4 selfTime: 0.2 |
| name: f ts: 2 dur: 0.2 selfTime: 0.2`); |
| }); |
| |
| it('parses a CPU profile without hitcounts', () => { |
| const profile = buildBasicProfile(); |
| for (const node of profile.nodes) { |
| node.hitCount = undefined; |
| } |
| const cpuProfileDataModel = new CPUProfile.CPUProfileDataModel.CPUProfileDataModel(profile); |
| const treeAsString = getFrameTreeAsString(cpuProfileDataModel); |
| assert.strictEqual(treeAsString, ` |
| name: a ts: 1.1 dur: 0.6 selfTime: 0.3 |
| name: c ts: 1.3 dur: 0.1 selfTime: 0.1 |
| name: d ts: 1.4 dur: 0.1 selfTime: 0.1 |
| name: c ts: 1.5 dur: 0.1 selfTime: 0.1 |
| name: b ts: 1.7 dur: 0.7 selfTime: 0.3 |
| name: e ts: 1.8 dur: 0.4 selfTime: 0.2 |
| name: f ts: 2 dur: 0.2 selfTime: 0.2`); |
| }); |
| it('fixes missing samples by replacing them with neighboring stacks', () => { |
| const scriptId = 'Peperoni' as Protocol.Runtime.ScriptId; |
| const url = ''; |
| const lineNumber = -1; |
| const columnNumber = -1; |
| |
| // The calls in the profile look roughly like: |
| // |
| // |program||-bar-||program||-bar-||program||-bar-||GC||program||-bar-||program||-bar-||-baz-||program||-bar-| |
| // |program| |program| |program||-foo-| |program||-foo-||program||-foo-| |program||-foo-| |
| // |
| // Which, after accounting for fixable program calls (missing samples), should look as (program samples are |
| // replaced with the preceding samples if the bottom frame of both neighboring samples is the same): |
| // |
| // |program||----------bar------||program||-------bar---------||-baz-||program||-bar-| |
| // |program| |-----foo----||program||-------foo---------| |program||-foo-| |
| // |-GC-| |
| const profile: Protocol.Profiler.Profile = { |
| startTime: 1000, |
| endTime: 4000, |
| nodes: [ |
| { |
| id: 1, |
| hitCount: 0, |
| callFrame: {functionName: '(root)', scriptId, url, lineNumber, columnNumber}, |
| children: [2, 3, 4, 5], |
| }, |
| { |
| id: 2, |
| hitCount: 1000, |
| callFrame: {functionName: '(garbage collector)', scriptId, url, lineNumber, columnNumber}, |
| }, |
| {id: 3, hitCount: 1000, callFrame: {functionName: '(program)', scriptId, url, lineNumber, columnNumber}}, |
| { |
| id: 4, |
| hitCount: 1000, |
| callFrame: {functionName: 'bar', scriptId, url, lineNumber, columnNumber}, |
| children: [6], |
| }, |
| {id: 5, hitCount: 1000, callFrame: {functionName: 'baz', scriptId, url, lineNumber, columnNumber}}, |
| {id: 6, hitCount: 1000, callFrame: {functionName: 'foo', scriptId, url, lineNumber, columnNumber}}, |
| ], |
| samples: [3, 4, 3, 4, 3, 6, 2, 2, 3, 6, 6, 3, 6, 5, 3, 6], |
| }; |
| profile.timeDeltas = profile.samples?.map(_ => 1000); |
| profile.endTime = profile.startTime + (profile.timeDeltas?.length || 0) * 1000; |
| const cpuProfileDataModel = new CPUProfile.CPUProfileDataModel.CPUProfileDataModel(profile); |
| const treeAsString = getFrameTreeAsString(cpuProfileDataModel); |
| assert.strictEqual(treeAsString, ` |
| name: (program) ts: 2 dur: 1 selfTime: 1 |
| name: bar ts: 3 dur: 7 selfTime: 4 |
| name: foo ts: 7 dur: 3 selfTime: 1 |
| name: (garbage collector) ts: 8 dur: 2 selfTime: 2 |
| name: (program) ts: 10 dur: 1 selfTime: 1 |
| name: bar ts: 11 dur: 4 selfTime: 0 |
| name: foo ts: 11 dur: 4 selfTime: 4 |
| name: baz ts: 15 dur: 1 selfTime: 1 |
| name: (program) ts: 16 dur: 1 selfTime: 1 |
| name: bar ts: 17 dur: 1 selfTime: 0 |
| name: foo ts: 17 dur: 1 selfTime: 1`); |
| }); |
| it('parses a CPU with GC nodes correctly', () => { |
| const scriptId = 'Peperoni' as Protocol.Runtime.ScriptId; |
| const url = ''; |
| const lineNumber = -1; |
| const columnNumber = -1; |
| |
| // Profile contains this tree: |
| // |
| // root |
| // / \ |
| // GC foo |
| // \ |
| // bar |
| |
| // The calls in the profile look roughly like: |
| // |
| // |-------------------foo----------------||--GC--| |
| // |---------bar--------| |--bar--| |
| // |
| // Which, after accounting for the GC call, should be fixed as: |
| // |-----------------------foo------------| |
| // |---bar---||---bar---| |--bar--| |
| // |--GC--| |
| const profile: Protocol.Profiler.Profile = { |
| startTime: 1000, |
| endTime: 4000, |
| nodes: [ |
| { |
| id: 1, |
| hitCount: 0, |
| callFrame: {functionName: '(root)', scriptId, url, lineNumber, columnNumber}, |
| children: [2, 3], |
| }, |
| { |
| id: 2, |
| hitCount: 1000, |
| callFrame: {functionName: '(garbage collector)', scriptId, url, lineNumber, columnNumber}, |
| }, |
| { |
| id: 3, |
| hitCount: 1000, |
| callFrame: {functionName: 'foo', scriptId, url, lineNumber, columnNumber}, |
| children: [4], |
| }, |
| {id: 4, hitCount: 1000, callFrame: {functionName: 'bar', scriptId, url, lineNumber, columnNumber}}, |
| ], |
| timeDeltas: [500, 250, 1000, 250, 1000], |
| samples: [4, 4, 3, 4, 2], |
| }; |
| const cpuProfileDataModel = new CPUProfile.CPUProfileDataModel.CPUProfileDataModel(profile); |
| const treeAsString = getFrameTreeAsString(cpuProfileDataModel); |
| assert.strictEqual(treeAsString, ` |
| name: foo ts: 1.5 dur: 3.13 selfTime: 0.25 |
| name: bar ts: 1.5 dur: 1.25 selfTime: 1.25 |
| name: bar ts: 3 dur: 1.63 selfTime: 1 |
| name: (garbage collector) ts: 4 dur: 0.63 selfTime: 0.63`); |
| }); |
| }); |