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 */
36WebInspector.FlameChart = function(cpuProfileView)
37{
38    WebInspector.View.call(this);
39    this.registerRequiredCSS("flameChart.css");
40    this.element.className = "fill";
41    this.element.id = "cpu-flame-chart";
42
43    this._overviewContainer = this.element.createChild("div", "overview-container");
44    this._overviewGrid = new WebInspector.OverviewGrid("flame-chart");
45    this._overviewContainer.appendChild(this._overviewGrid.element);
46    this._overviewCalculator = new WebInspector.FlameChart.OverviewCalculator();
47    this._overviewGrid.addEventListener(WebInspector.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this);
48    this._overviewCanvas = this._overviewContainer.createChild("canvas");
49
50    this._chartContainer = this.element.createChild("div", "chart-container");
51    this._timelineGrid = new WebInspector.TimelineGrid();
52    this._chartContainer.appendChild(this._timelineGrid.element);
53    this._calculator = new WebInspector.FlameChart.Calculator();
54
55    this._canvas = this._chartContainer.createChild("canvas");
56    WebInspector.installDragHandle(this._canvas, this._startCanvasDragging.bind(this), this._canvasDragging.bind(this), this._endCanvasDragging.bind(this), "col-resize");
57
58    this._cpuProfileView = cpuProfileView;
59    this._windowLeft = 0.0;
60    this._windowRight = 1.0;
61    this._barHeight = 15;
62    this._minWidth = 1;
63    this._paddingLeft = 15;
64    this._canvas.addEventListener("mousewheel", this._onMouseWheel.bind(this), false);
65    this.element.addEventListener("click", this._onClick.bind(this), false);
66    this._popoverHelper = new WebInspector.PopoverHelper(this._chartContainer, this._getPopoverAnchor.bind(this), this._showPopover.bind(this));
67    this._popoverHelper.setTimeout(250);
68    this._linkifier = new WebInspector.Linkifier();
69    this._highlightedNodeIndex = -1;
70
71    if (!WebInspector.FlameChart._colorGenerator)
72        WebInspector.FlameChart._colorGenerator = new WebInspector.FlameChart.ColorGenerator();
73}
74
75/**
76 * @constructor            entries.push(new WebInspector.FlameChart.Entry(colorPair, level, node.totalTime, offset, node));
77/
78 * @implements {WebInspector.TimelineGrid.Calculator}
79 */
80WebInspector.FlameChart.Calculator = function()
81{
82}
83
84WebInspector.FlameChart.Calculator.prototype = {
85    /**
86     * @param {WebInspector.FlameChart} flameChart
87     */
88    _updateBoundaries: function(flameChart)
89    {
90        this._minimumBoundaries = flameChart._windowLeft * flameChart._timelineData.totalTime;
91        this._maximumBoundaries = flameChart._windowRight * flameChart._timelineData.totalTime;
92        this._paddingLeft = flameChart._paddingLeft;
93        this._width = flameChart._canvas.width - this._paddingLeft;
94        this._timeToPixel = this._width / this.boundarySpan();
95    },
96
97    /**
98     * @param {number} time
99     */
100    computePosition: function(time)
101    {
102        return (time - this._minimumBoundaries) * this._timeToPixel + this._paddingLeft;
103    },
104
105    formatTime: function(value)
106    {
107        return Number.secondsToString((value + this._minimumBoundaries) / 1000);
108    },
109
110    maximumBoundary: function()
111    {
112        return this._maximumBoundaries;
113    },
114
115    minimumBoundary: function()
116    {
117        return this._minimumBoundaries;
118    },
119
120    zeroTime: function()
121    {
122        return 0;
123    },
124
125    boundarySpan: function()
126    {
127        return this._maximumBoundaries - this._minimumBoundaries;
128    }
129}
130
131/**
132 * @constructor
133 * @implements {WebInspector.TimelineGrid.Calculator}
134 */
135WebInspector.FlameChart.OverviewCalculator = function()
136{
137}
138
139WebInspector.FlameChart.OverviewCalculator.prototype = {
140    /**
141     * @param {WebInspector.FlameChart} flameChart
142     */
143    _updateBoundaries: function(flameChart)
144    {
145        this._minimumBoundaries = 0;
146        this._maximumBoundaries = flameChart._timelineData.totalTime;
147        this._xScaleFactor = flameChart._canvas.width / flameChart._timelineData.totalTime;
148    },
149
150    /**
151     * @param {number} time
152     */
153    computePosition: function(time)
154    {
155        return (time - this._minimumBoundaries) * this._xScaleFactor;
156    },
157
158    formatTime: function(value)
159    {
160        return Number.secondsToString((value + this._minimumBoundaries) / 1000);
161    },
162
163    maximumBoundary: function()
164    {
165        return this._maximumBoundaries;
166    },
167
168    minimumBoundary: function()
169    {
170        return this._minimumBoundaries;
171    },
172
173    zeroTime: function()
174    {
175        return this._minimumBoundaries;
176    },
177
178    boundarySpan: function()
179    {
180        return this._maximumBoundaries - this._minimumBoundaries;
181    }
182}
183
184WebInspector.FlameChart.Events = {
185    SelectedNode: "SelectedNode"
186}
187
188/**
189 * @constructor
190 */
191WebInspector.FlameChart.ColorGenerator = function()
192{
193    this._colorPairs = {};
194    this._currentColorIndex = 0;
195}
196
197WebInspector.FlameChart.ColorGenerator.prototype = {
198    /**
199     * @param {!string} id
200     */
201    _colorPairForID: function(id)
202    {
203        var colorPairs = this._colorPairs;
204        var colorPair = colorPairs[id];
205        if (!colorPair) {
206            var currentColorIndex = ++this._currentColorIndex;
207            var hue = (currentColorIndex * 5 + 11 * (currentColorIndex % 2)) % 360;
208            colorPairs[id] = colorPair = {highlighted: "hsla(" + hue + ", 100%, 33%, 0.7)", normal: "hsla(" + hue + ", 100%, 66%, 0.7)"};
209        }
210        return colorPair;
211    }
212}
213
214/**
215 * @constructor
216 * @param {!Object} colorPair
217 * @param {!number} depth
218 * @param {!number} duration
219 * @param {!number} startTime
220 * @param {Object} node
221 */
222WebInspector.FlameChart.Entry = function(colorPair, depth, duration, startTime, node)
223{
224    this.colorPair = colorPair;
225    this.depth = depth;
226    this.duration = duration;
227    this.startTime = startTime;
228    this.node = node;
229}
230
231WebInspector.FlameChart.prototype = {
232    _onWindowChanged: function(event)
233    {
234        this._hidePopover();
235        this._scheduleUpdate();
236    },
237
238    _startCanvasDragging: function(event)
239    {
240        if (!this._timelineData)
241            return false;
242        this._isDragging = true;
243        this._dragStartPoint = event.pageX;
244        this._dragStartWindowLeft = this._windowLeft;
245        this._dragStartWindowRight = this._windowRight;
246        this._hidePopover();
247        return true;
248    },
249
250    _canvasDragging: function(event)
251    {
252        var pixelShift = this._dragStartPoint - event.pageX;
253        var windowShift = pixelShift / this._totalPixels;
254
255        var windowLeft = Math.max(0, this._dragStartWindowLeft + windowShift);
256        if (windowLeft === this._windowLeft)
257            return;
258        windowShift = windowLeft - this._dragStartWindowLeft;
259
260        var windowRight = Math.min(1, this._dragStartWindowRight + windowShift);
261        if (windowRight === this._windowRight)
262            return;
263        windowShift = windowRight - this._dragStartWindowRight;
264        this._overviewGrid.setWindow(this._dragStartWindowLeft + windowShift, this._dragStartWindowRight + windowShift);
265    },
266
267    _endCanvasDragging: function()
268    {
269        this._isDragging = false;
270    },
271
272    _calculateTimelineData: function()
273    {
274        if (this._cpuProfileView.samples)
275            return this._calculateTimelineDataForSamples();
276
277        if (this._timelineData)
278            return this._timelineData;
279
280        if (!this._cpuProfileView.profileHead)
281            return null;
282
283        var index = 0;
284        var entries = [];
285
286        function appendReversedArray(toArray, fromArray)
287        {
288            for (var i = fromArray.length - 1; i >= 0; --i)
289                toArray.push(fromArray[i]);
290        }
291
292        var stack = [];
293        appendReversedArray(stack, this._cpuProfileView.profileHead.children);
294
295        var levelOffsets = /** @type {Array.<!number>} */ ([0]);
296        var levelExitIndexes = /** @type {Array.<!number>} */ ([0]);
297        var colorGenerator = WebInspector.FlameChart._colorGenerator;
298
299        while (stack.length) {
300            var level = levelOffsets.length - 1;
301            var node = stack.pop();
302            var offset = levelOffsets[level];
303
304            var colorPair = colorGenerator._colorPairForID(node.functionName + ":" + node.url + ":" + node.lineNumber);
305
306            entries.push(new WebInspector.FlameChart.Entry(colorPair, level, node.totalTime, offset, node));
307
308            ++index;
309
310            levelOffsets[level] += node.totalTime;
311            if (node.children.length) {
312                levelExitIndexes.push(stack.length);
313                levelOffsets.push(offset + node.selfTime / 2);
314                appendReversedArray(stack, node.children);
315            }
316
317            while (stack.length === levelExitIndexes[levelExitIndexes.length - 1]) {
318                levelOffsets.pop();
319                levelExitIndexes.pop();
320            }
321        }
322
323        this._timelineData = {
324            entries: entries,
325            totalTime: this._cpuProfileView.profileHead.totalTime,
326        }
327
328        return this._timelineData;
329    },
330
331    _calculateTimelineDataForSamples: function()
332    {
333        if (this._timelineData)
334            return this._timelineData;
335
336        if (!this._cpuProfileView.profileHead)
337            return null;
338
339        var samples = this._cpuProfileView.samples;
340        var idToNode = this._cpuProfileView._idToNode;
341        var samplesCount = samples.length;
342
343        var index = 0;
344        var entries = /** @type {Array.<!WebInspector.FlameChart.Entry>} */ ([]);
345
346        var openIntervals = [];
347        var stackTrace = [];
348        var colorGenerator = WebInspector.FlameChart._colorGenerator;
349        for (var sampleIndex = 0; sampleIndex < samplesCount; sampleIndex++) {
350            var node = idToNode[samples[sampleIndex]];
351            stackTrace.length = 0;
352            while (node) {
353                stackTrace.push(node);
354                node = node.parent;
355            }
356            stackTrace.pop(); // Remove (root) node
357
358            var depth = 0;
359            node = stackTrace.pop();
360            while (node && depth < openIntervals.length && node === openIntervals[depth].node) {
361                var intervalIndex = openIntervals[depth].index;
362                entries[intervalIndex].duration += 1;
363                node = stackTrace.pop();
364                ++depth;
365            }
366            if (depth < openIntervals.length)
367                openIntervals.length = depth;
368            if (!node)
369                continue;
370
371            while (node) {
372                var colorPair = colorGenerator._colorPairForID(node.functionName + ":" + node.url + ":" + node.lineNumber);
373
374                entries.push(new WebInspector.FlameChart.Entry(colorPair, depth, 1, sampleIndex, node));
375                openIntervals.push({node: node, index: index});
376                ++index;
377
378                node = stackTrace.pop();
379                ++depth;
380            }
381        }
382
383        this._timelineData = {
384            entries: entries,
385            totalTime: samplesCount,
386        };
387
388        return this._timelineData;
389    },
390
391    _getPopoverAnchor: function(element, event)
392    {
393        if (this._isDragging)
394            return null;
395
396        var nodeIndex = this._coordinatesToNodeIndex(event.offsetX, event.offsetY);
397
398        this._highlightedNodeIndex = nodeIndex;
399        this.update();
400
401        if (nodeIndex === -1)
402            return null;
403
404        var anchorBox = new AnchorBox();
405        this._entryToAnchorBox(this._timelineData.entries[nodeIndex], anchorBox);
406        anchorBox.x += event.pageX - event.offsetX;
407        anchorBox.y += event.pageY - event.offsetY;
408
409        return anchorBox;
410    },
411
412    _showPopover: function(anchor, popover)
413    {
414        if (this._isDragging)
415            return;
416        var node = this._timelineData.entries[this._highlightedNodeIndex].node;
417        if (!node)
418            return;
419        var contentHelper = new WebInspector.PopoverContentHelper(node.functionName);
420        contentHelper.appendTextRow(WebInspector.UIString("Total time"), Number.secondsToString(node.totalTime / 1000, true));
421        contentHelper.appendTextRow(WebInspector.UIString("Self time"), Number.secondsToString(node.selfTime / 1000, true));
422        if (node.numberOfCalls)
423            contentHelper.appendTextRow(WebInspector.UIString("Number of calls"), node.numberOfCalls);
424        if (node.url) {
425            var link = this._linkifier.linkifyLocation(node.url, node.lineNumber);
426            contentHelper.appendElementRow("Location", link);
427        }
428
429        popover.show(contentHelper._contentTable, anchor);
430    },
431
432    _hidePopover: function()
433    {
434        this._popoverHelper.hidePopover();
435        this._linkifier.reset();
436    },
437
438    _onClick: function(e)
439    {
440        if (this._highlightedNodeIndex === -1)
441            return;
442        var node = this._timelineData.entries[this._highlightedNodeIndex].node;
443        this.dispatchEventToListeners(WebInspector.FlameChart.Events.SelectedNode, node);
444    },
445
446    _onMouseWheel: function(e)
447    {
448        var zoomFactor = (e.wheelDelta > 0) ? 0.9 : 1.1;
449        var windowPoint = (this._pixelWindowLeft + e.offsetX) / this._totalPixels;
450        var overviewReferencePoint = Math.floor(windowPoint * this._pixelWindowWidth);
451        this._overviewGrid.zoom(zoomFactor, overviewReferencePoint);
452        this._hidePopover();
453    },
454
455    /**
456     * @param {!number} x
457     * @param {!number} y
458     */
459    _coordinatesToNodeIndex: function(x, y)
460    {
461        var timelineData = this._timelineData;
462        if (!timelineData)
463            return -1;
464        var timelineEntries = timelineData.entries;
465        var cursorTime = (x + this._pixelWindowLeft - this._paddingLeft) * this._pixelToTime;
466        var cursorLevel = Math.floor((this._canvas.height - y) / this._barHeight);
467
468        for (var i = 0; i < timelineEntries.length; ++i) {
469            if (cursorTime < timelineEntries[i].startTime)
470                return -1;
471            if (cursorTime < (timelineEntries[i].startTime + timelineEntries[i].duration)
472                && cursorLevel === timelineEntries[i].depth)
473                return i;
474        }
475        return -1;
476    },
477
478    onResize: function()
479    {
480        this._updateOverviewCanvas = true;
481        this._hidePopover();
482        this._scheduleUpdate();
483    },
484
485    _drawOverviewCanvas: function(width, height)
486    {
487        this._overviewCanvas.width = width;
488        this._overviewCanvas.height = height;
489
490        if (!this._timelineData)
491            return;
492
493        var timelineEntries = this._timelineData.entries;
494
495        var drawData = new Uint8Array(width);
496        var scaleFactor = width / this._totalTime;
497
498        for (var nodeIndex = 0; nodeIndex < timelineEntries.length; ++nodeIndex) {
499            var entry = timelineEntries[nodeIndex];
500            var start = Math.floor(entry.startTime * scaleFactor);
501            var finish = Math.floor((entry.startTime + entry.duration) * scaleFactor);
502            for (var x = start; x < finish; ++x)
503                drawData[x] = Math.max(drawData[x], entry.depth + 1);
504        }
505
506        var context = this._overviewCanvas.getContext("2d");
507        var yScaleFactor = 2;
508        context.lineWidth = 0.5;
509        context.strokeStyle = "rgba(20,0,0,0.8)";
510        context.fillStyle="rgba(214,225,254, 0.8)";
511        context.moveTo(0, height - 1);
512        for (var x = 0; x < width; ++x)
513            context.lineTo(x, height - drawData[x] * yScaleFactor - 1);
514        context.moveTo(width - 1, height - 1);
515        context.moveTo(0, height - 1);
516        context.fill();
517        context.stroke();
518        context.closePath();
519    },
520
521    /**
522     * @param {WebInspector.FlameChart.Entry} entry
523     * @param {AnchorBox} anchorBox
524     */
525    _entryToAnchorBox: function(entry, anchorBox)
526    {
527        anchorBox.x = Math.floor(entry.startTime * this._timeToPixel) - this._pixelWindowLeft + this._paddingLeft;
528        anchorBox.y = this._canvas.height - (entry.depth + 1) * this._barHeight;
529        anchorBox.width = Math.floor(entry.duration * this._timeToPixel);
530        anchorBox.height = this._barHeight;
531        if (anchorBox.x < 0) {
532            anchorBox.width += anchorBox.x;
533            anchorBox.x = 0;
534        }
535        anchorBox.width = Number.constrain(anchorBox.width, 0, this._canvas.width - anchorBox.x);
536    },
537
538    /**
539     * @param {!number} height
540     * @param {!number} width
541     */
542    draw: function(width, height)
543    {
544        var timelineData = this._calculateTimelineData();
545        if (!timelineData)
546            return;
547        var timelineEntries = timelineData.entries;
548        this._canvas.height = height;
549        this._canvas.width = width;
550        var barHeight = this._barHeight;
551
552        var context = this._canvas.getContext("2d");
553        var textPaddingLeft = 2;
554        var paddingLeft = this._paddingLeft;
555        context.font = (barHeight - 3) + "px sans-serif";
556        context.textBaseline = "top";
557        this._dotsWidth = context.measureText("\u2026").width;
558
559        var anchorBox = new AnchorBox();
560        for (var i = 0; i < timelineEntries.length; ++i) {
561            var entry = timelineEntries[i];
562            var startTime = entry.startTime;
563            if (startTime > this._timeWindowRight)
564                break;
565            if ((startTime + entry.duration) < this._timeWindowLeft)
566                continue;
567            this._entryToAnchorBox(entry, anchorBox);
568            if (anchorBox.width < this._minWidth)
569                continue;
570
571            var colorPair = entry.colorPair;
572            var color;
573            if (this._highlightedNodeIndex === i)
574                color =  colorPair.highlighted;
575            else
576                color = colorPair.normal;
577
578            context.beginPath();
579            context.rect(anchorBox.x, anchorBox.y, anchorBox.width - 1, anchorBox.height - 1);
580            context.fillStyle = color;
581            context.fill();
582
583            var xText = Math.max(0, anchorBox.x);
584            var widthText = anchorBox.width - textPaddingLeft + anchorBox.x - xText;
585            var title = this._prepareTitle(context, entry.node.functionName, widthText);
586            if (title) {
587                context.fillStyle = "#333";
588                context.fillText(title, xText + textPaddingLeft, anchorBox.y - 1);
589            }
590        }
591    },
592
593    _prepareTitle: function(context, title, maxSize)
594    {
595        if (maxSize < this._dotsWidth)
596            return null;
597        var titleWidth = context.measureText(title).width;
598        if (maxSize > titleWidth)
599            return title;
600        maxSize -= this._dotsWidth;
601        var dotRegExp=/[\.\$]/g;
602        var match = dotRegExp.exec(title);
603        if (!match) {
604            var visiblePartSize = maxSize / titleWidth;
605            var newTextLength = Math.floor(title.length * visiblePartSize) + 1;
606            var minTextLength = 4;
607            if (newTextLength < minTextLength)
608                return null;
609            var substring;
610            do {
611                --newTextLength;
612                substring = title.substring(0, newTextLength);
613            } while (context.measureText(substring).width > maxSize);
614            return title.substring(0, newTextLength) + "\u2026";
615        }
616        while (match) {
617            var substring = title.substring(match.index + 1);
618            var width = context.measureText(substring).width;
619            if (maxSize > width)
620                return "\u2026" + substring;
621            match = dotRegExp.exec(title);
622        }
623    },
624
625    _scheduleUpdate: function()
626    {
627        if (this._updateTimerId)
628            return;
629        this._updateTimerId = setTimeout(this.update.bind(this), 10);
630    },
631
632    _updateBoundaries: function()
633    {
634        this._windowLeft = this._overviewGrid.windowLeft();
635        this._windowRight = this._overviewGrid.windowRight();
636        this._windowWidth = this._windowRight - this._windowLeft;
637
638        this._totalTime = this._timelineData.totalTime;
639        this._timeWindowLeft = this._windowLeft * this._totalTime;
640        this._timeWindowRight = this._windowRight * this._totalTime;
641
642        this._pixelWindowWidth = this._chartContainer.clientWidth;
643        this._totalPixels = Math.floor(this._pixelWindowWidth / this._windowWidth);
644        this._pixelWindowLeft = Math.floor(this._totalPixels * this._windowLeft);
645        this._pixelWindowRight = Math.floor(this._totalPixels * this._windowRight);
646
647        this._timeToPixel = this._totalPixels / this._totalTime;
648        this._pixelToTime = this._totalTime / this._totalPixels;
649    },
650
651    update: function()
652    {
653        this._updateTimerId = 0;
654        if (!this._timelineData)
655            this._calculateTimelineData();
656        if (!this._timelineData)
657            return;
658        this._updateBoundaries();
659        this.draw(this._chartContainer.clientWidth, this._chartContainer.clientHeight);
660        this._calculator._updateBoundaries(this);
661        this._overviewCalculator._updateBoundaries(this);
662        this._timelineGrid.element.style.width = this.element.clientWidth;
663        this._timelineGrid.updateDividers(this._calculator);
664        this._overviewGrid.updateDividers(this._overviewCalculator);
665        if (this._updateOverviewCanvas) {
666            this._drawOverviewCanvas(this._overviewContainer.clientWidth, this._overviewContainer.clientHeight);
667            this._updateOverviewCanvas = false;
668        }
669    },
670
671    __proto__: WebInspector.View.prototype
672};
673