1// Copyright 2014 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5 6/** 7 * @constructor 8 * @param {!ProfilerAgent.CPUProfile} profile 9 */ 10WebInspector.CPUProfileDataModel = function(profile) 11{ 12 this.profileHead = profile.head; 13 this.samples = profile.samples; 14 this.timestamps = profile.timestamps; 15 this.profileStartTime = profile.startTime * 1000; 16 this.profileEndTime = profile.endTime * 1000; 17 this._assignParentsInProfile(); 18 if (this.samples) { 19 this._normalizeTimestamps(); 20 this._buildIdToNodeMap(); 21 this._fixMissingSamples(); 22 } 23 this._calculateTimes(profile); 24} 25 26WebInspector.CPUProfileDataModel.prototype = { 27 /** 28 * @param {!ProfilerAgent.CPUProfile} profile 29 */ 30 _calculateTimes: function(profile) 31 { 32 function totalHitCount(node) { 33 var result = node.hitCount; 34 for (var i = 0; i < node.children.length; i++) 35 result += totalHitCount(node.children[i]); 36 return result; 37 } 38 profile.totalHitCount = totalHitCount(profile.head); 39 40 var duration = this.profileEndTime - this.profileStartTime; 41 var samplingInterval = duration / profile.totalHitCount; 42 this.samplingInterval = samplingInterval; 43 44 function calculateTimesForNode(node) { 45 node.selfTime = node.hitCount * samplingInterval; 46 var totalHitCount = node.hitCount; 47 for (var i = 0; i < node.children.length; i++) 48 totalHitCount += calculateTimesForNode(node.children[i]); 49 node.totalTime = totalHitCount * samplingInterval; 50 return totalHitCount; 51 } 52 calculateTimesForNode(profile.head); 53 }, 54 55 _assignParentsInProfile: function() 56 { 57 var head = this.profileHead; 58 head.parent = null; 59 head.depth = -1; 60 this.maxDepth = 0; 61 var nodesToTraverse = [ head ]; 62 while (nodesToTraverse.length) { 63 var parent = nodesToTraverse.pop(); 64 var depth = parent.depth + 1; 65 if (depth > this.maxDepth) 66 this.maxDepth = depth; 67 var children = parent.children; 68 var length = children.length; 69 for (var i = 0; i < length; ++i) { 70 var child = children[i]; 71 child.parent = parent; 72 child.depth = depth; 73 if (child.children.length) 74 nodesToTraverse.push(child); 75 } 76 } 77 }, 78 79 _normalizeTimestamps: function() 80 { 81 var timestamps = this.timestamps; 82 if (!timestamps) { 83 // Support loading old CPU profiles that are missing timestamps. 84 // Derive timestamps from profile start and stop times. 85 var profileStartTime = this.profileStartTime; 86 var interval = (this.profileEndTime - profileStartTime) / this.samples.length; 87 timestamps = new Float64Array(this.samples.length + 1); 88 for (var i = 0; i < timestamps.length; ++i) 89 timestamps[i] = profileStartTime + i * interval; 90 this.timestamps = timestamps; 91 return; 92 } 93 94 // Convert samples from usec to msec 95 for (var i = 0; i < timestamps.length; ++i) 96 timestamps[i] /= 1000; 97 var averageSample = (timestamps.peekLast() - timestamps[0]) / (timestamps.length - 1); 98 // Add an extra timestamp used to calculate the last sample duration. 99 this.timestamps.push(timestamps.peekLast() + averageSample); 100 this.profileStartTime = timestamps[0]; 101 this.profileEndTime = timestamps.peekLast(); 102 }, 103 104 _buildIdToNodeMap: function() 105 { 106 /** @type {!Object.<number, !ProfilerAgent.CPUProfileNode>} */ 107 this._idToNode = {}; 108 var idToNode = this._idToNode; 109 var stack = [this.profileHead]; 110 while (stack.length) { 111 var node = stack.pop(); 112 idToNode[node.id] = node; 113 for (var i = 0; i < node.children.length; i++) 114 stack.push(node.children[i]); 115 } 116 117 var topLevelNodes = this.profileHead.children; 118 for (var i = 0; i < topLevelNodes.length && !(this.gcNode && this.programNode && this.idleNode); i++) { 119 var node = topLevelNodes[i]; 120 if (node.functionName === "(garbage collector)") 121 this.gcNode = node; 122 else if (node.functionName === "(program)") 123 this.programNode = node; 124 else if (node.functionName === "(idle)") 125 this.idleNode = node; 126 } 127 }, 128 129 _fixMissingSamples: function() 130 { 131 // Sometimes sampler is not able to parse the JS stack and returns 132 // a (program) sample instead. The issue leads to call frames belong 133 // to the same function invocation being split apart. 134 // Here's a workaround for that. When there's a single (program) sample 135 // between two call stacks sharing the same bottom node, it is replaced 136 // with the preceeding sample. 137 var samples = this.samples; 138 var samplesCount = samples.length; 139 if (!this.programNode || samplesCount < 3) 140 return; 141 var idToNode = this._idToNode; 142 var programNodeId = this.programNode.id; 143 var gcNodeId = this.gcNode ? this.gcNode.id : -1; 144 var idleNodeId = this.idleNode ? this.idleNode.id : -1; 145 var prevNodeId = samples[0]; 146 var nodeId = samples[1]; 147 for (var sampleIndex = 1; sampleIndex < samplesCount - 1; sampleIndex++) { 148 var nextNodeId = samples[sampleIndex + 1]; 149 if (nodeId === programNodeId && !isSystemNode(prevNodeId) && !isSystemNode(nextNodeId) 150 && bottomNode(idToNode[prevNodeId]) === bottomNode(idToNode[nextNodeId])) { 151 samples[sampleIndex] = prevNodeId; 152 } 153 prevNodeId = nodeId; 154 nodeId = nextNodeId; 155 } 156 157 /** 158 * @param {!ProfilerAgent.CPUProfileNode} node 159 * @return {!ProfilerAgent.CPUProfileNode} 160 */ 161 function bottomNode(node) 162 { 163 while (node.parent) 164 node = node.parent; 165 return node; 166 } 167 168 /** 169 * @param {number} nodeId 170 * @return {boolean} 171 */ 172 function isSystemNode(nodeId) 173 { 174 return nodeId === programNodeId || nodeId === gcNodeId || nodeId === idleNodeId; 175 } 176 }, 177 178 /** 179 * @param {function(number, !ProfilerAgent.CPUProfileNode, number)} openFrameCallback 180 * @param {function(number, !ProfilerAgent.CPUProfileNode, number, number, number)} closeFrameCallback 181 * @param {number=} startTime 182 * @param {number=} stopTime 183 */ 184 forEachFrame: function(openFrameCallback, closeFrameCallback, startTime, stopTime) 185 { 186 if (!this.profileHead) 187 return; 188 189 startTime = startTime || 0; 190 stopTime = stopTime || Infinity; 191 var samples = this.samples; 192 var timestamps = this.timestamps; 193 var idToNode = this._idToNode; 194 var gcNode = this.gcNode; 195 var samplesCount = samples.length; 196 var startIndex = timestamps.lowerBound(startTime); 197 var stackTop = 0; 198 var stackNodes = []; 199 var prevId = this.profileHead.id; 200 var prevHeight = this.profileHead.depth; 201 var sampleTime = timestamps[samplesCount]; 202 var gcParentNode = null; 203 204 if (!this._stackStartTimes) 205 this._stackStartTimes = new Float64Array(this.maxDepth + 2); 206 var stackStartTimes = this._stackStartTimes; 207 if (!this._stackChildrenDuration) 208 this._stackChildrenDuration = new Float64Array(this.maxDepth + 2); 209 var stackChildrenDuration = this._stackChildrenDuration; 210 211 for (var sampleIndex = startIndex; sampleIndex < samplesCount; sampleIndex++) { 212 sampleTime = timestamps[sampleIndex]; 213 if (sampleTime >= stopTime) 214 break; 215 var id = samples[sampleIndex]; 216 if (id === prevId) 217 continue; 218 var node = idToNode[id]; 219 var prevNode = idToNode[prevId]; 220 221 if (node === gcNode) { 222 // GC samples have no stack, so we just put GC node on top of the last recorded sample. 223 gcParentNode = prevNode; 224 openFrameCallback(gcParentNode.depth + 1, gcNode, sampleTime); 225 stackStartTimes[++stackTop] = sampleTime; 226 stackChildrenDuration[stackTop] = 0; 227 prevId = id; 228 continue; 229 } 230 if (prevNode === gcNode) { 231 // end of GC frame 232 var start = stackStartTimes[stackTop]; 233 var duration = sampleTime - start; 234 stackChildrenDuration[stackTop - 1] += duration; 235 closeFrameCallback(gcParentNode.depth + 1, gcNode, start, duration, duration - stackChildrenDuration[stackTop]); 236 --stackTop; 237 prevNode = gcParentNode; 238 prevId = prevNode.id; 239 gcParentNode = null; 240 } 241 242 while (node.depth > prevNode.depth) { 243 stackNodes.push(node); 244 node = node.parent; 245 } 246 247 // Go down to the LCA and close current intervals. 248 while (prevNode !== node) { 249 var start = stackStartTimes[stackTop]; 250 var duration = sampleTime - start; 251 stackChildrenDuration[stackTop - 1] += duration; 252 closeFrameCallback(prevNode.depth, prevNode, start, duration, duration - stackChildrenDuration[stackTop]); 253 --stackTop; 254 if (node.depth === prevNode.depth) { 255 stackNodes.push(node); 256 node = node.parent; 257 } 258 prevNode = prevNode.parent; 259 } 260 261 // Go up the nodes stack and open new intervals. 262 while (stackNodes.length) { 263 node = stackNodes.pop(); 264 openFrameCallback(node.depth, node, sampleTime); 265 stackStartTimes[++stackTop] = sampleTime; 266 stackChildrenDuration[stackTop] = 0; 267 } 268 269 prevId = id; 270 } 271 272 if (idToNode[prevId] === gcNode) { 273 var start = stackStartTimes[stackTop]; 274 var duration = sampleTime - start; 275 stackChildrenDuration[stackTop - 1] += duration; 276 closeFrameCallback(gcParentNode.depth + 1, node, start, duration, duration - stackChildrenDuration[stackTop]); 277 --stackTop; 278 } 279 280 for (var node = idToNode[prevId]; node.parent; node = node.parent) { 281 var start = stackStartTimes[stackTop]; 282 var duration = sampleTime - start; 283 stackChildrenDuration[stackTop - 1] += duration; 284 closeFrameCallback(node.depth, node, start, duration, duration - stackChildrenDuration[stackTop]); 285 --stackTop; 286 } 287 } 288} 289