• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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