|  | // 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; |