blob: 846b50df795c3020091b65aae2dbc85b15583171 [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.
/**
* @constructor
* @param {!ProfilerAgent.CPUProfile} profile
*/
WebInspector.CPUProfileDataModel = function(profile)
{
this.profileHead = profile.head;
this.samples = profile.samples;
this.timestamps = profile.timestamps;
this.profileStartTime = profile.startTime * 1000;
this.profileEndTime = profile.endTime * 1000;
this._assignParentsInProfile();
if (this.samples) {
this._normalizeTimestamps();
this._buildIdToNodeMap();
this._fixMissingSamples();
}
this._calculateTimes(profile);
}
/**
* @param {string} name
* @return {string}
*/
WebInspector.CPUProfileDataModel.beautifyFunctionName = function(name)
{
return name || WebInspector.UIString("(anonymous function)");
}
WebInspector.CPUProfileDataModel.prototype = {
/**
* @param {!ProfilerAgent.CPUProfile} profile
*/
_calculateTimes: function(profile)
{
function totalHitCount(node) {
var result = node.hitCount;
for (var i = 0; i < node.children.length; i++)
result += totalHitCount(node.children[i]);
return result;
}
profile.totalHitCount = totalHitCount(profile.head);
var duration = this.profileEndTime - this.profileStartTime;
var samplingInterval = duration / profile.totalHitCount;
this.samplingInterval = samplingInterval;
function calculateTimesForNode(node) {
node.selfTime = node.hitCount * samplingInterval;
var totalHitCount = node.hitCount;
for (var i = 0; i < node.children.length; i++)
totalHitCount += calculateTimesForNode(node.children[i]);
node.totalTime = totalHitCount * samplingInterval;
return totalHitCount;
}
calculateTimesForNode(profile.head);
},
_assignParentsInProfile: function()
{
var head = this.profileHead;
head.parent = null;
head.depth = -1;
this.maxDepth = 0;
var nodesToTraverse = [ head ];
while (nodesToTraverse.length) {
var parent = nodesToTraverse.pop();
var depth = parent.depth + 1;
if (depth > this.maxDepth)
this.maxDepth = depth;
var children = parent.children;
var length = children.length;
for (var i = 0; i < length; ++i) {
var child = children[i];
child.parent = parent;
child.depth = depth;
if (child.children.length)
nodesToTraverse.push(child);
}
}
},
_normalizeTimestamps: function()
{
var timestamps = this.timestamps;
if (!timestamps) {
// Support loading old CPU profiles that are missing timestamps.
// Derive timestamps from profile start and stop times.
var profileStartTime = this.profileStartTime;
var interval = (this.profileEndTime - profileStartTime) / this.samples.length;
timestamps = new Float64Array(this.samples.length + 1);
for (var i = 0; i < timestamps.length; ++i)
timestamps[i] = profileStartTime + i * interval;
this.timestamps = timestamps;
return;
}
// Convert samples from usec to msec
for (var i = 0; i < timestamps.length; ++i)
timestamps[i] /= 1000;
var averageSample = (timestamps.peekLast() - timestamps[0]) / (timestamps.length - 1);
// Add an extra timestamp used to calculate the last sample duration.
this.timestamps.push(timestamps.peekLast() + averageSample);
this.profileStartTime = timestamps[0];
this.profileEndTime = timestamps.peekLast();
},
_buildIdToNodeMap: function()
{
/** @type {!Object.<number, !ProfilerAgent.CPUProfileNode>} */
this._idToNode = {};
var idToNode = this._idToNode;
var stack = [this.profileHead];
while (stack.length) {
var node = stack.pop();
idToNode[node.id] = node;
for (var i = 0; i < node.children.length; i++)
stack.push(node.children[i]);
}
var topLevelNodes = this.profileHead.children;
for (var i = 0; i < topLevelNodes.length && !(this.gcNode && this.programNode && this.idleNode); i++) {
var 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: function()
{
// 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.
var samples = this.samples;
var samplesCount = samples.length;
if (!this.programNode || samplesCount < 3)
return;
var idToNode = this._idToNode;
var programNodeId = this.programNode.id;
var gcNodeId = this.gcNode ? this.gcNode.id : -1;
var idleNodeId = this.idleNode ? this.idleNode.id : -1;
var prevNodeId = samples[0];
var nodeId = samples[1];
for (var sampleIndex = 1; sampleIndex < samplesCount - 1; sampleIndex++) {
var nextNodeId = samples[sampleIndex + 1];
if (nodeId === programNodeId && !isSystemNode(prevNodeId) && !isSystemNode(nextNodeId)
&& bottomNode(idToNode[prevNodeId]) === bottomNode(idToNode[nextNodeId])) {
samples[sampleIndex] = prevNodeId;
}
prevNodeId = nodeId;
nodeId = nextNodeId;
}
/**
* @param {!ProfilerAgent.CPUProfileNode} node
* @return {!ProfilerAgent.CPUProfileNode}
*/
function bottomNode(node)
{
while (node.parent)
node = node.parent;
return node;
}
/**
* @param {number} nodeId
* @return {boolean}
*/
function isSystemNode(nodeId)
{
return nodeId === programNodeId || nodeId === gcNodeId || nodeId === idleNodeId;
}
},
/**
* @param {function(number, !ProfilerAgent.CPUProfileNode, number)} openFrameCallback
* @param {function(number, !ProfilerAgent.CPUProfileNode, number, number, number)} closeFrameCallback
* @param {number=} startTime
* @param {number=} stopTime
*/
forEachFrame: function(openFrameCallback, closeFrameCallback, startTime, stopTime)
{
if (!this.profileHead)
return;
startTime = startTime || 0;
stopTime = stopTime || Infinity;
var samples = this.samples;
var timestamps = this.timestamps;
var idToNode = this._idToNode;
var gcNode = this.gcNode;
var samplesCount = samples.length;
var startIndex = timestamps.lowerBound(startTime);
var stackTop = 0;
var stackNodes = [];
var prevId = this.profileHead.id;
var prevHeight = this.profileHead.depth;
var sampleTime = timestamps[samplesCount];
var gcParentNode = null;
if (!this._stackStartTimes)
this._stackStartTimes = new Float64Array(this.maxDepth + 2);
var stackStartTimes = this._stackStartTimes;
if (!this._stackChildrenDuration)
this._stackChildrenDuration = new Float64Array(this.maxDepth + 2);
var stackChildrenDuration = this._stackChildrenDuration;
for (var sampleIndex = startIndex; sampleIndex < samplesCount; sampleIndex++) {
sampleTime = timestamps[sampleIndex];
if (sampleTime >= stopTime)
break;
var id = samples[sampleIndex];
if (id === prevId)
continue;
var node = idToNode[id];
var prevNode = idToNode[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
var start = stackStartTimes[stackTop];
var 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) {
var start = stackStartTimes[stackTop];
var duration = sampleTime - start;
stackChildrenDuration[stackTop - 1] += duration;
closeFrameCallback(prevNode.depth, 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;
}
if (idToNode[prevId] === gcNode) {
var start = stackStartTimes[stackTop];
var duration = sampleTime - start;
stackChildrenDuration[stackTop - 1] += duration;
closeFrameCallback(gcParentNode.depth + 1, node, start, duration, duration - stackChildrenDuration[stackTop]);
--stackTop;
}
for (var node = idToNode[prevId]; node.parent; node = node.parent) {
var start = stackStartTimes[stackTop];
var duration = sampleTime - start;
stackChildrenDuration[stackTop - 1] += duration;
closeFrameCallback(node.depth, node, start, duration, duration - stackChildrenDuration[stackTop]);
--stackTop;
}
},
/**
* @param {number} index
* @return {!ProfilerAgent.CPUProfileNode}
*/
nodeByIndex: function(index)
{
return this._idToNode[this.samples[index]];
}
}