blob: 14659d548e58521d3d5e3e43e9d6d793315ad411 [file] [log] [blame]
// 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;