Web Inspector: implement Flame Chart for CPU profiler.
[WebKit-https.git] / Source / WebCore / inspector / front-end / FlameChart.js
1 /*
2  * Copyright (C) 2013 Google Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are
6  * met:
7  *
8  *     * Redistributions of source code must retain the above copyright
9  * notice, this list of conditions and the following disclaimer.
10  *     * Redistributions in binary form must reproduce the above
11  * copyright notice, this list of conditions and the following disclaimer
12  * in the documentation and/or other materials provided with the
13  * distribution.
14  *     * Neither the name of Google Inc. nor the names of its
15  * contributors may be used to endorse or promote products derived from
16  * this software without specific prior written permission.
17  *
18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29  */
30
31 /**
32  * @constructor
33  * @extends {WebInspector.View}
34  * @param {WebInspector.CPUProfileView} cpuProfileView
35  */
36 WebInspector.FlameChart = function(cpuProfileView)
37 {
38     WebInspector.View.call(this);
39     this.registerRequiredCSS("flameChart.css");
40
41     this.element.className = "flame-chart";
42     this._canvas = this.element.createChild("canvas");
43     this._cpuProfileView = cpuProfileView;
44     this._xScaleFactor = 0.0;
45     this._yScaleFactor = 10;
46     this._minWidth = 3;
47     this.element.onmousemove = this._onMouseMove.bind(this);
48 }
49
50 WebInspector.FlameChart.prototype = {
51     _onMouseMove: function(e)
52     {
53         var node = this._coordinatesToNode(e.offsetX, e.offsetY);
54         if (node !== this._highlightedNode) {
55             this._highlightedNode = node;
56             this.update();
57         }
58     },
59
60     /**
61      * @param {!number} x
62      * @param {!number} y
63      */
64     _coordinatesToNode: function(x, y)
65     {
66         var cursorOffset = x / this._xScaleFactor;
67         var cursorLevel = (this._canvas.height - y) / this._yScaleFactor;
68         this._highlightedNode = null;
69         var cursorNode;
70
71         function findNodeCallback(offset, level, node)
72         {
73             if (cursorLevel > level && cursorLevel < level + 1 && cursorOffset > offset && cursorOffset < offset + node.totalTime)
74                 cursorNode = node;
75         }
76         this._forEachNode(findNodeCallback.bind(this));
77         return cursorNode;
78     },
79
80     onResize: function()
81     {
82         this.draw(this.element.clientWidth, this.element.clientHeight);
83     },
84
85     /**
86      * @return {Array.<!ProfilerAgent.CPUProfileNode> }
87      */
88     _rootNodes: function()
89     {
90         if (this._rootNodesArray)
91             return this._rootNodesArray.slice();
92
93         var profileHead = this._cpuProfileView.profileHead;
94         if (!profileHead)
95             return null;
96         var totalTime = 0;
97         var nodes = [];
98         for (var i = 0; i < profileHead.children.length; ++i) {
99             var node = profileHead.children[i];
100             if (node.functionName === "(program)" || node.functionName === "(idle)")
101                 continue;
102             totalTime += node.totalTime;
103             nodes.push(node);
104         }
105
106         this._rootNodesArray = nodes;
107         this._totalTime = totalTime;
108         return nodes.slice();
109     },
110
111     /**
112      * @param {!number} height
113      * @param {!number} width
114      */
115     draw: function(width, height)
116     {
117         if (!this._rootNodes())
118             return;
119
120         var margin = 0;
121         this._canvas.height = height - margin;
122         this._canvas.width = width - margin;
123
124         this._xScaleFactor = width / this._totalTime;
125         this._colorIndex = 0;
126
127         this._context = this._canvas.getContext("2d");
128
129         this._forEachNode(this._drawNode.bind(this));
130     },
131
132     _drawNode: function(offset, level, node)
133     {
134         ++this._colorIndex;
135         var hue = (this._colorIndex * 2 + 11 * (this._colorIndex % 2)) % 360;
136         var lightness = this._highlightedNode === node ? 33 : 67;
137         var color = "hsl(" + hue + ", 100%, " + lightness + "%)";
138         this._drawBar(this._context, offset, level, node, color);
139     },
140
141     _forEachNode: function(callback)
142     {
143         var nodes = this._rootNodes();
144         var levelOffsets = /** @type {Array.<!number>} */ [0];
145         var levelExitIndexes = [0];
146
147         while (nodes.length) {
148             var level = levelOffsets.length - 1;
149             var node = nodes.pop();
150             if (node.totalTime * this._xScaleFactor > this._minWidth) {
151                 var offset = levelOffsets[level];
152                 callback(offset, level, node);
153                 levelOffsets[level] += node.totalTime;
154                 if (node.children.length) {
155                     levelExitIndexes.push(nodes.length);
156                     levelOffsets.push(offset + node.selfTime / 2);
157                     nodes = nodes.concat(node.children);
158                 }
159             }
160             while (nodes.length === levelExitIndexes[levelExitIndexes.length - 1]) {
161                 levelOffsets.pop();
162                 levelExitIndexes.pop();
163             }
164         }
165     },
166
167     /**
168      * @param {Object} context
169      * @param {number} offset
170      * @param {number} level
171      * @param {!ProfilerAgent.CPUProfileNode} node
172      * @param {!WebInspector.Color} hslColor
173      */
174     _drawBar: function(context, offset, level, node, hslColor)
175     {
176         var width = node.totalTime * this._xScaleFactor;
177         var height = this._yScaleFactor;
178         var x = offset * this._xScaleFactor;
179         var y = this._canvas.height - level * this._yScaleFactor - height;
180         context.beginPath();
181         context.rect(x, y, width - 1, height - 1);
182         context.fillStyle = hslColor;
183         context.fill();
184     },
185
186     update: function()
187     {
188         this.draw(this.element.clientWidth, this.element.clientHeight);
189     },
190
191     __proto__: WebInspector.View.prototype
192 };
193
194 //@ sourceURL=http://localhost/inspector/front-end/FlameChart.js