| // Copyright 2014 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. |
| /** |
| * @unrestricted |
| */ |
| export class CPUProfileNode extends SDK.ProfileNode { |
| /** |
| * @param {!Protocol.Profiler.ProfileNode} node |
| * @param {number} sampleTime |
| */ |
| constructor(node, sampleTime) { |
| const callFrame = node.callFrame || /** @type {!Protocol.Runtime.CallFrame} */ ({ |
| // Backward compatibility for old SamplingHeapProfileNode format. |
| functionName: node['functionName'], |
| scriptId: node['scriptId'], |
| url: node['url'], |
| lineNumber: node['lineNumber'] - 1, |
| columnNumber: node['columnNumber'] - 1 |
| }); |
| super(callFrame); |
| this.id = node.id; |
| this.self = node.hitCount * sampleTime; |
| this.positionTicks = node.positionTicks; |
| // Compatibility: legacy backends could provide "no reason" for optimized functions. |
| this.deoptReason = node.deoptReason && node.deoptReason !== 'no reason' ? node.deoptReason : null; |
| } |
| } |
| |
| /** |
| * @unrestricted |
| */ |
| export default class CPUProfileDataModel extends SDK.ProfileTreeModel { |
| /** |
| * @param {!Protocol.Profiler.Profile} profile |
| * @param {?SDK.Target} target |
| */ |
| constructor(profile, target) { |
| super(target); |
| const isLegacyFormat = !!profile['head']; |
| if (isLegacyFormat) { |
| // Legacy format contains raw timestamps and start/stop times are in seconds. |
| this.profileStartTime = profile.startTime * 1000; |
| this.profileEndTime = profile.endTime * 1000; |
| this.timestamps = profile.timestamps; |
| this._compatibilityConversionHeadToNodes(profile); |
| } else { |
| // Current format encodes timestamps as deltas. Start/stop times are in microseconds. |
| this.profileStartTime = profile.startTime / 1000; |
| this.profileEndTime = profile.endTime / 1000; |
| this.timestamps = this._convertTimeDeltas(profile); |
| } |
| this.samples = profile.samples; |
| this.lines = profile.lines; |
| this.totalHitCount = 0; |
| this.profileHead = this._translateProfileTree(profile.nodes); |
| this.initialize(this.profileHead); |
| this._extractMetaNodes(); |
| if (this.samples) { |
| this._buildIdToNodeMap(); |
| this._sortSamples(); |
| this._normalizeTimestamps(); |
| this._fixMissingSamples(); |
| } |
| } |
| |
| /** |
| * @param {!Protocol.Profiler.Profile} profile |
| */ |
| _compatibilityConversionHeadToNodes(profile) { |
| if (!profile.head || profile.nodes) { |
| return; |
| } |
| /** @type {!Array<!Protocol.Profiler.ProfileNode>} */ |
| const nodes = []; |
| convertNodesTree(profile.head); |
| profile.nodes = nodes; |
| delete profile.head; |
| /** |
| * @param {!Protocol.Profiler.ProfileNode} node |
| * @return {number} |
| */ |
| function convertNodesTree(node) { |
| nodes.push(node); |
| node.children = (/** @type {!Array<!Protocol.Profiler.ProfileNode>} */ (node.children)).map(convertNodesTree); |
| return node.id; |
| } |
| } |
| |
| /** |
| * @param {!Protocol.Profiler.Profile} profile |
| * @return {?Array<number>} |
| */ |
| _convertTimeDeltas(profile) { |
| if (!profile.timeDeltas) { |
| return null; |
| } |
| let lastTimeUsec = profile.startTime; |
| const timestamps = new Array(profile.timeDeltas.length); |
| for (let i = 0; i < profile.timeDeltas.length; ++i) { |
| lastTimeUsec += profile.timeDeltas[i]; |
| timestamps[i] = lastTimeUsec; |
| } |
| return timestamps; |
| } |
| |
| /** |
| * @param {!Array<!Protocol.Profiler.ProfileNode>} nodes |
| * @return {!CPUProfileNode} |
| */ |
| _translateProfileTree(nodes) { |
| /** |
| * @param {!Protocol.Profiler.ProfileNode} node |
| * @return {boolean} |
| */ |
| function isNativeNode(node) { |
| if (node.callFrame) { |
| return !!node.callFrame.url && node.callFrame.url.startsWith('native '); |
| } |
| return !!node['url'] && node['url'].startsWith('native '); |
| } |
| |
| /** |
| * @param {!Array<!Protocol.Profiler.ProfileNode>} nodes |
| */ |
| function buildChildrenFromParents(nodes) { |
| if (nodes[0].children) { |
| return; |
| } |
| nodes[0].children = []; |
| for (let i = 1; i < nodes.length; ++i) { |
| const node = nodes[i]; |
| const parentNode = nodeByIdMap.get(node.parent); |
| if (parentNode.children) { |
| parentNode.children.push(node.id); |
| } else { |
| parentNode.children = [node.id]; |
| } |
| } |
| } |
| |
| /** |
| * @param {!Array<!Protocol.Profiler.ProfileNode>} nodes |
| * @param {!Array<number>|undefined} samples |
| */ |
| function buildHitCountFromSamples(nodes, samples) { |
| if (typeof (nodes[0].hitCount) === 'number') { |
| return; |
| } |
| console.assert(samples, 'Error: Neither hitCount nor samples are present in profile.'); |
| for (let i = 0; i < nodes.length; ++i) { |
| nodes[i].hitCount = 0; |
| } |
| for (let i = 0; i < samples.length; ++i) { |
| ++nodeByIdMap.get(samples[i]).hitCount; |
| } |
| } |
| |
| /** @type {!Map<number, !Protocol.Profiler.ProfileNode>} */ |
| const nodeByIdMap = new Map(); |
| for (let i = 0; i < nodes.length; ++i) { |
| const node = nodes[i]; |
| nodeByIdMap.set(node.id, node); |
| } |
| |
| buildHitCountFromSamples(nodes, this.samples); |
| buildChildrenFromParents(nodes); |
| this.totalHitCount = nodes.reduce((acc, node) => acc + node.hitCount, 0); |
| const sampleTime = (this.profileEndTime - this.profileStartTime) / this.totalHitCount; |
| const keepNatives = !!Common.moduleSetting('showNativeFunctionsInJSProfile').get(); |
| const root = nodes[0]; |
| /** @type {!Map<number, number>} */ |
| const idMap = new Map([[root.id, root.id]]); |
| const resultRoot = new CPUProfileNode(root, sampleTime); |
| const parentNodeStack = root.children.map(() => resultRoot); |
| const sourceNodeStack = root.children.map(id => nodeByIdMap.get(id)); |
| while (sourceNodeStack.length) { |
| let parentNode = parentNodeStack.pop(); |
| const sourceNode = sourceNodeStack.pop(); |
| if (!sourceNode.children) { |
| sourceNode.children = []; |
| } |
| const targetNode = new CPUProfileNode(sourceNode, sampleTime); |
| if (keepNatives || !isNativeNode(sourceNode)) { |
| parentNode.children.push(targetNode); |
| parentNode = targetNode; |
| } else { |
| parentNode.self += targetNode.self; |
| } |
| idMap.set(sourceNode.id, parentNode.id); |
| parentNodeStack.push.apply(parentNodeStack, sourceNode.children.map(() => parentNode)); |
| sourceNodeStack.push.apply(sourceNodeStack, sourceNode.children.map(id => nodeByIdMap.get(id))); |
| } |
| if (this.samples) { |
| this.samples = this.samples.map(id => idMap.get(id)); |
| } |
| return resultRoot; |
| } |
| |
| _sortSamples() { |
| const timestamps = this.timestamps; |
| if (!timestamps) { |
| return; |
| } |
| const samples = this.samples; |
| const indices = timestamps.map((x, index) => index); |
| indices.sort((a, b) => timestamps[a] - timestamps[b]); |
| for (let i = 0; i < timestamps.length; ++i) { |
| let index = indices[i]; |
| if (index === i) { |
| continue; |
| } |
| // Move items in a cycle. |
| const savedTimestamp = timestamps[i]; |
| const savedSample = samples[i]; |
| let currentIndex = i; |
| while (index !== i) { |
| samples[currentIndex] = samples[index]; |
| timestamps[currentIndex] = timestamps[index]; |
| currentIndex = index; |
| index = indices[index]; |
| indices[currentIndex] = currentIndex; |
| } |
| samples[currentIndex] = savedSample; |
| timestamps[currentIndex] = savedTimestamp; |
| } |
| } |
| |
| _normalizeTimestamps() { |
| let timestamps = this.timestamps; |
| if (!timestamps) { |
| // Support loading old CPU profiles that are missing timestamps. |
| // Derive timestamps from profile start and stop times. |
| const profileStartTime = this.profileStartTime; |
| const interval = (this.profileEndTime - profileStartTime) / this.samples.length; |
| timestamps = new Float64Array(this.samples.length + 1); |
| for (let i = 0; i < timestamps.length; ++i) { |
| timestamps[i] = profileStartTime + i * interval; |
| } |
| this.timestamps = timestamps; |
| return; |
| } |
| |
| // Convert samples from usec to msec |
| for (let i = 0; i < timestamps.length; ++i) { |
| timestamps[i] /= 1000; |
| } |
| if (this.samples.length === timestamps.length) { |
| // Support for a legacy format where were no timeDeltas. |
| // Add an extra timestamp used to calculate the last sample duration. |
| const averageSample = (timestamps.peekLast() - timestamps[0]) / (timestamps.length - 1); |
| this.timestamps.push(timestamps.peekLast() + averageSample); |
| } |
| this.profileStartTime = timestamps[0]; |
| this.profileEndTime = timestamps.peekLast(); |
| } |
| |
| _buildIdToNodeMap() { |
| /** @type {!Map<number, !CPUProfileNode>} */ |
| this._idToNode = new Map(); |
| const idToNode = this._idToNode; |
| const stack = [this.profileHead]; |
| while (stack.length) { |
| const node = stack.pop(); |
| idToNode.set(node.id, node); |
| stack.push.apply(stack, node.children); |
| } |
| } |
| |
| _extractMetaNodes() { |
| const topLevelNodes = this.profileHead.children; |
| for (let i = 0; i < topLevelNodes.length && !(this.gcNode && this.programNode && this.idleNode); i++) { |
| const node = topLevelNodes[i]; |
| if (node.functionName === '(garbage collector)') { |
| this.gcNode = node; |
| } else if (node.functionName === '(program)') { |
| this.programNode = node; |
| } else if (node.functionName === '(idle)') { |
| this.idleNode = node; |
| } |
| } |
| } |
| |
| _fixMissingSamples() { |
| // Sometimes sampler is not able to parse the JS stack and returns |
| // a (program) sample instead. The issue leads to call frames belong |
| // to the same function invocation being split apart. |
| // Here's a workaround for that. When there's a single (program) sample |
| // between two call stacks sharing the same bottom node, it is replaced |
| // with the preceeding sample. |
| const samples = this.samples; |
| const samplesCount = samples.length; |
| if (!this.programNode || samplesCount < 3) { |
| return; |
| } |
| const idToNode = this._idToNode; |
| const programNodeId = this.programNode.id; |
| const gcNodeId = this.gcNode ? this.gcNode.id : -1; |
| const idleNodeId = this.idleNode ? this.idleNode.id : -1; |
| let prevNodeId = samples[0]; |
| let nodeId = samples[1]; |
| let count = 0; |
| for (let sampleIndex = 1; sampleIndex < samplesCount - 1; sampleIndex++) { |
| const nextNodeId = samples[sampleIndex + 1]; |
| if (nodeId === programNodeId && !isSystemNode(prevNodeId) && !isSystemNode(nextNodeId) && |
| bottomNode(idToNode.get(prevNodeId)) === bottomNode(idToNode.get(nextNodeId))) { |
| ++count; |
| samples[sampleIndex] = prevNodeId; |
| } |
| prevNodeId = nodeId; |
| nodeId = nextNodeId; |
| } |
| if (count) { |
| Common.console.warn(ls`DevTools: CPU profile parser is fixing ${count} missing samples.`); |
| } |
| /** |
| * @param {!SDK.ProfileNode} node |
| * @return {!SDK.ProfileNode} |
| */ |
| function bottomNode(node) { |
| while (node.parent && node.parent.parent) { |
| node = node.parent; |
| } |
| return node; |
| } |
| /** |
| * @param {number} nodeId |
| * @return {boolean} |
| */ |
| function isSystemNode(nodeId) { |
| return nodeId === programNodeId || nodeId === gcNodeId || nodeId === idleNodeId; |
| } |
| } |
| |
| /** |
| * @param {function(number, !CPUProfileNode, number)} openFrameCallback |
| * @param {function(number, !CPUProfileNode, number, number, number)} closeFrameCallback |
| * @param {number=} startTime |
| * @param {number=} stopTime |
| */ |
| forEachFrame(openFrameCallback, closeFrameCallback, startTime, stopTime) { |
| if (!this.profileHead || !this.samples) { |
| return; |
| } |
| |
| startTime = startTime || 0; |
| stopTime = stopTime || Infinity; |
| const samples = this.samples; |
| const timestamps = this.timestamps; |
| const idToNode = this._idToNode; |
| const gcNode = this.gcNode; |
| const samplesCount = samples.length; |
| const startIndex = timestamps.lowerBound(startTime); |
| let stackTop = 0; |
| const stackNodes = []; |
| let prevId = this.profileHead.id; |
| let sampleTime; |
| let gcParentNode = null; |
| |
| // Extra slots for gc being put on top, |
| // and one at the bottom to allow safe stackTop-1 access. |
| const stackDepth = this.maxDepth + 3; |
| if (!this._stackStartTimes) { |
| this._stackStartTimes = new Float64Array(stackDepth); |
| } |
| const stackStartTimes = this._stackStartTimes; |
| if (!this._stackChildrenDuration) { |
| this._stackChildrenDuration = new Float64Array(stackDepth); |
| } |
| const stackChildrenDuration = this._stackChildrenDuration; |
| |
| let node; |
| let sampleIndex; |
| for (sampleIndex = startIndex; sampleIndex < samplesCount; sampleIndex++) { |
| sampleTime = timestamps[sampleIndex]; |
| if (sampleTime >= stopTime) { |
| break; |
| } |
| const id = samples[sampleIndex]; |
| if (id === prevId) { |
| continue; |
| } |
| node = idToNode.get(id); |
| let prevNode = idToNode.get(prevId); |
| |
| if (node === gcNode) { |
| // GC samples have no stack, so we just put GC node on top of the last recorded sample. |
| gcParentNode = prevNode; |
| openFrameCallback(gcParentNode.depth + 1, gcNode, sampleTime); |
| stackStartTimes[++stackTop] = sampleTime; |
| stackChildrenDuration[stackTop] = 0; |
| prevId = id; |
| continue; |
| } |
| if (prevNode === gcNode) { |
| // end of GC frame |
| const start = stackStartTimes[stackTop]; |
| const duration = sampleTime - start; |
| stackChildrenDuration[stackTop - 1] += duration; |
| closeFrameCallback(gcParentNode.depth + 1, gcNode, start, duration, duration - stackChildrenDuration[stackTop]); |
| --stackTop; |
| prevNode = gcParentNode; |
| prevId = prevNode.id; |
| gcParentNode = null; |
| } |
| |
| while (node.depth > prevNode.depth) { |
| stackNodes.push(node); |
| node = node.parent; |
| } |
| |
| // Go down to the LCA and close current intervals. |
| while (prevNode !== node) { |
| const start = stackStartTimes[stackTop]; |
| const duration = sampleTime - start; |
| stackChildrenDuration[stackTop - 1] += duration; |
| closeFrameCallback( |
| prevNode.depth, /** @type {!CPUProfileNode} */ (prevNode), start, duration, |
| duration - stackChildrenDuration[stackTop]); |
| --stackTop; |
| if (node.depth === prevNode.depth) { |
| stackNodes.push(node); |
| node = node.parent; |
| } |
| prevNode = prevNode.parent; |
| } |
| |
| // Go up the nodes stack and open new intervals. |
| while (stackNodes.length) { |
| node = stackNodes.pop(); |
| openFrameCallback(node.depth, node, sampleTime); |
| stackStartTimes[++stackTop] = sampleTime; |
| stackChildrenDuration[stackTop] = 0; |
| } |
| |
| prevId = id; |
| } |
| |
| sampleTime = timestamps[sampleIndex] || this.profileEndTime; |
| if (idToNode.get(prevId) === gcNode) { |
| const start = stackStartTimes[stackTop]; |
| const duration = sampleTime - start; |
| stackChildrenDuration[stackTop - 1] += duration; |
| closeFrameCallback(gcParentNode.depth + 1, node, start, duration, duration - stackChildrenDuration[stackTop]); |
| --stackTop; |
| prevId = gcParentNode.id; |
| } |
| |
| for (let node = idToNode.get(prevId); node.parent; node = node.parent) { |
| const start = stackStartTimes[stackTop]; |
| const duration = sampleTime - start; |
| stackChildrenDuration[stackTop - 1] += duration; |
| closeFrameCallback( |
| node.depth, /** @type {!CPUProfileNode} */ (node), start, duration, |
| duration - stackChildrenDuration[stackTop]); |
| --stackTop; |
| } |
| } |
| |
| /** |
| * @param {number} index |
| * @return {?CPUProfileNode} |
| */ |
| nodeByIndex(index) { |
| return this._idToNode.get(this.samples[index]) || null; |
| } |
| } |
| |
| /* Legacy exported object */ |
| self.SDK = self.SDK || {}; |
| |
| /* Legacy exported object */ |
| SDK = SDK || {}; |
| |
| /** @constructor */ |
| SDK.CPUProfileDataModel = CPUProfileDataModel; |
| |
| /** @constructor */ |
| SDK.CPUProfileNode = CPUProfileNode; |