1/**
2 * Copyright (C) 2014 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/**
33 * @constructor
34 * @implements {WebInspector.FlameChartDataProvider}
35 * @param {!WebInspector.CPUProfileDataModel} cpuProfile
36 * @param {?WebInspector.Target} target
37 */
38WebInspector.CPUFlameChartDataProvider = function(cpuProfile, target)
39{
40    WebInspector.FlameChartDataProvider.call(this);
41    this._cpuProfile = cpuProfile;
42    this._target = target;
43    this._colorGenerator = WebInspector.CPUFlameChartDataProvider.colorGenerator();
44}
45
46WebInspector.CPUFlameChartDataProvider.prototype = {
47    /**
48     * @return {number}
49     */
50    barHeight: function()
51    {
52        return 15;
53    },
54
55    /**
56     * @return {number}
57     */
58    textBaseline: function()
59    {
60        return 4;
61    },
62
63    /**
64     * @return {number}
65     */
66    textPadding: function()
67    {
68        return 2;
69    },
70
71    /**
72     * @param {number} startTime
73     * @param {number} endTime
74     * @return {?Array.<number>}
75     */
76    dividerOffsets: function(startTime, endTime)
77    {
78        return null;
79    },
80
81    /**
82     * @return {number}
83     */
84    minimumBoundary: function()
85    {
86        return this._cpuProfile.profileStartTime;
87    },
88
89    /**
90     * @return {number}
91     */
92    totalTime: function()
93    {
94        return this._cpuProfile.profileHead.totalTime;
95    },
96
97    /**
98     * @return {number}
99     */
100    maxStackDepth: function()
101    {
102        return this._maxStackDepth;
103    },
104
105    /**
106     * @return {?WebInspector.FlameChart.TimelineData}
107     */
108    timelineData: function()
109    {
110        return this._timelineData || this._calculateTimelineData();
111    },
112
113    /**
114     * @param {number} index
115     * @return {string}
116     */
117    markerColor: function(index)
118    {
119        throw new Error("Unreachable.");
120    },
121
122    /**
123     * @param {number} index
124     * @return {string}
125     */
126    markerTitle: function(index)
127    {
128        throw new Error("Unreachable.");
129    },
130
131    /**
132     * @return {?WebInspector.FlameChart.TimelineData}
133     */
134    _calculateTimelineData: function()
135    {
136        /**
137         * @constructor
138         * @param {number} depth
139         * @param {number} duration
140         * @param {number} startTime
141         * @param {number} selfTime
142         * @param {!ProfilerAgent.CPUProfileNode} node
143         */
144        function ChartEntry(depth, duration, startTime, selfTime, node)
145        {
146            this.depth = depth;
147            this.duration = duration;
148            this.startTime = startTime;
149            this.selfTime = selfTime;
150            this.node = node;
151        }
152
153        /** @type {!Array.<?ChartEntry>} */
154        var entries = [];
155        /** @type {!Array.<number>} */
156        var stack = [];
157        var maxDepth = 5;
158
159        function onOpenFrame()
160        {
161            stack.push(entries.length);
162            // Reserve space for the entry, as they have to be ordered by startTime.
163            // The entry itself will be put there in onCloseFrame.
164            entries.push(null);
165        }
166        function onCloseFrame(depth, node, startTime, totalTime, selfTime)
167        {
168            var index = stack.pop();
169            entries[index] = new ChartEntry(depth, totalTime, startTime, selfTime, node);
170            maxDepth = Math.max(maxDepth, depth);
171        }
172        this._cpuProfile.forEachFrame(onOpenFrame, onCloseFrame);
173
174        /** @type {!Array.<!ProfilerAgent.CPUProfileNode>} */
175        var entryNodes = new Array(entries.length);
176        var entryLevels = new Uint8Array(entries.length);
177        var entryTotalTimes = new Float32Array(entries.length);
178        var entrySelfTimes = new Float32Array(entries.length);
179        var entryStartTimes = new Float64Array(entries.length);
180        var minimumBoundary = this.minimumBoundary();
181
182        for (var i = 0; i < entries.length; ++i) {
183            var entry = entries[i];
184            entryNodes[i] = entry.node;
185            entryLevels[i] = entry.depth;
186            entryTotalTimes[i] = entry.duration;
187            entryStartTimes[i] = entry.startTime;
188            entrySelfTimes[i] = entry.selfTime;
189        }
190
191        this._maxStackDepth = maxDepth;
192
193        this._timelineData = new WebInspector.FlameChart.TimelineData(entryLevels, entryTotalTimes, entryStartTimes);
194
195        /** @type {!Array.<!ProfilerAgent.CPUProfileNode>} */
196        this._entryNodes = entryNodes;
197        this._entrySelfTimes = entrySelfTimes;
198
199        return this._timelineData;
200    },
201
202    /**
203     * @param {number} ms
204     * @return {string}
205     */
206    _millisecondsToString: function(ms)
207    {
208        if (ms === 0)
209            return "0";
210        if (ms < 1000)
211            return WebInspector.UIString("%.1f\u2009ms", ms);
212        return Number.secondsToString(ms / 1000, true);
213    },
214
215    /**
216     * @param {number} entryIndex
217     * @return {?Array.<!{title: string, text: string}>}
218     */
219    prepareHighlightedEntryInfo: function(entryIndex)
220    {
221        var timelineData = this._timelineData;
222        var node = this._entryNodes[entryIndex];
223        if (!node)
224            return null;
225
226        var entryInfo = [];
227        function pushEntryInfoRow(title, text)
228        {
229            var row = {};
230            row.title = title;
231            row.text = text;
232            entryInfo.push(row);
233        }
234
235        var name = WebInspector.CPUProfileDataModel.beautifyFunctionName(node.functionName);
236        pushEntryInfoRow(WebInspector.UIString("Name"), name);
237        var selfTime = this._millisecondsToString(this._entrySelfTimes[entryIndex]);
238        var totalTime = this._millisecondsToString(timelineData.entryTotalTimes[entryIndex]);
239        pushEntryInfoRow(WebInspector.UIString("Self time"), selfTime);
240        pushEntryInfoRow(WebInspector.UIString("Total time"), totalTime);
241        var text = this._target ? WebInspector.Linkifier.liveLocationText(this._target, node.scriptId, node.lineNumber, node.columnNumber) : node.url;
242        pushEntryInfoRow(WebInspector.UIString("URL"), text);
243        pushEntryInfoRow(WebInspector.UIString("Aggregated self time"), Number.secondsToString(node.selfTime / 1000, true));
244        pushEntryInfoRow(WebInspector.UIString("Aggregated total time"), Number.secondsToString(node.totalTime / 1000, true));
245        if (node.deoptReason && node.deoptReason !== "no reason")
246            pushEntryInfoRow(WebInspector.UIString("Not optimized"), node.deoptReason);
247
248        return entryInfo;
249    },
250
251    /**
252     * @param {number} entryIndex
253     * @return {boolean}
254     */
255    canJumpToEntry: function(entryIndex)
256    {
257        return this._entryNodes[entryIndex].scriptId !== "0";
258    },
259
260    /**
261     * @param {number} entryIndex
262     * @return {?string}
263     */
264    entryTitle: function(entryIndex)
265    {
266        var node = this._entryNodes[entryIndex];
267        return WebInspector.CPUProfileDataModel.beautifyFunctionName(node.functionName);
268    },
269
270    /**
271     * @param {number} entryIndex
272     * @return {?string}
273     */
274    entryFont: function(entryIndex)
275    {
276        if (!this._font) {
277            this._font = (this.barHeight() - 4) + "px " + WebInspector.fontFamily();
278            this._boldFont = "bold " + this._font;
279        }
280        var node = this._entryNodes[entryIndex];
281        var reason = node.deoptReason;
282        return (reason && reason !== "no reason") ? this._boldFont : this._font;
283    },
284
285    /**
286     * @param {number} entryIndex
287     * @return {string}
288     */
289    entryColor: function(entryIndex)
290    {
291        var node = this._entryNodes[entryIndex];
292        return this._colorGenerator.colorForID(node.functionName + ":" + node.url);
293    },
294
295    /**
296     * @param {number} entryIndex
297     * @param {!CanvasRenderingContext2D} context
298     * @param {?string} text
299     * @param {number} barX
300     * @param {number} barY
301     * @param {number} barWidth
302     * @param {number} barHeight
303     * @param {function(number):number} timeToPosition
304     * @return {boolean}
305     */
306    decorateEntry: function(entryIndex, context, text, barX, barY, barWidth, barHeight, timeToPosition)
307    {
308        return false;
309    },
310
311    /**
312     * @param {number} entryIndex
313     * @return {boolean}
314     */
315    forceDecoration: function(entryIndex)
316    {
317        return false;
318    },
319
320    /**
321     * @param {number} entryIndex
322     * @return {!{startTime: number, endTime: number}}
323     */
324    highlightTimeRange: function(entryIndex)
325    {
326        var startTime = this._timelineData.entryStartTimes[entryIndex];
327        return {
328            startTime: startTime,
329            endTime: startTime + this._timelineData.entryTotalTimes[entryIndex]
330        };
331    },
332
333    /**
334     * @return {number}
335     */
336    paddingLeft: function()
337    {
338        return 15;
339    },
340
341    /**
342     * @param {number} entryIndex
343     * @return {string}
344     */
345    textColor: function(entryIndex)
346    {
347        return "#333";
348    }
349}
350
351
352/**
353 * @return {!WebInspector.FlameChart.ColorGenerator}
354 */
355WebInspector.CPUFlameChartDataProvider.colorGenerator = function()
356{
357    if (!WebInspector.CPUFlameChartDataProvider._colorGenerator) {
358        var colorGenerator = new WebInspector.FlameChart.ColorGenerator(
359            { min: 180, max: 310, count: 7 },
360            { min: 50, max: 80, count: 5 },
361            { min: 80, max: 90, count: 3 });
362        colorGenerator.setColorForID("(idle):", "hsl(0, 0%, 94%)");
363        colorGenerator.setColorForID("(program):", "hsl(0, 0%, 80%)");
364        colorGenerator.setColorForID("(garbage collector):", "hsl(0, 0%, 80%)");
365        WebInspector.CPUFlameChartDataProvider._colorGenerator = colorGenerator;
366    }
367    return WebInspector.CPUFlameChartDataProvider._colorGenerator;
368}
369
370
371/**
372 * @constructor
373 * @extends {WebInspector.VBox}
374 * @param {!WebInspector.FlameChartDataProvider} dataProvider
375 */
376WebInspector.CPUProfileFlameChart = function(dataProvider)
377{
378    WebInspector.VBox.call(this);
379    this.registerRequiredCSS("flameChart.css");
380    this.element.id = "cpu-flame-chart";
381
382    this._overviewPane = new WebInspector.CPUProfileFlameChart.OverviewPane(dataProvider);
383    this._overviewPane.show(this.element);
384
385    this._mainPane = new WebInspector.FlameChart(dataProvider, this._overviewPane, true);
386    this._mainPane.show(this.element);
387    this._mainPane.addEventListener(WebInspector.FlameChart.Events.EntrySelected, this._onEntrySelected, this);
388    this._overviewPane.addEventListener(WebInspector.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this);
389}
390
391WebInspector.CPUProfileFlameChart.prototype = {
392    /**
393     * @param {!WebInspector.Event} event
394     */
395    _onWindowChanged: function(event)
396    {
397        var windowLeft = event.data.windowTimeLeft;
398        var windowRight = event.data.windowTimeRight;
399        this._mainPane.setWindowTimes(windowLeft, windowRight);
400    },
401
402    /**
403     * @param {!number} timeLeft
404     * @param {!number} timeRight
405     */
406    selectRange: function(timeLeft, timeRight)
407    {
408        this._overviewPane._selectRange(timeLeft, timeRight);
409    },
410
411    /**
412     * @param {!WebInspector.Event} event
413     */
414    _onEntrySelected: function(event)
415    {
416        this.dispatchEventToListeners(WebInspector.FlameChart.Events.EntrySelected, event.data);
417    },
418
419    update: function()
420    {
421        this._overviewPane.update();
422        this._mainPane.update();
423    },
424
425    __proto__: WebInspector.VBox.prototype
426};
427
428/**
429 * @constructor
430 * @implements {WebInspector.TimelineGrid.Calculator}
431 */
432WebInspector.CPUProfileFlameChart.OverviewCalculator = function()
433{
434}
435
436WebInspector.CPUProfileFlameChart.OverviewCalculator.prototype = {
437    /**
438     * @return {number}
439     */
440    paddingLeft: function()
441    {
442        return 0;
443    },
444
445    /**
446     * @param {!WebInspector.CPUProfileFlameChart.OverviewPane} overviewPane
447     */
448    _updateBoundaries: function(overviewPane)
449    {
450        this._minimumBoundaries = overviewPane._dataProvider.minimumBoundary();
451        var totalTime = overviewPane._dataProvider.totalTime();
452        this._maximumBoundaries = this._minimumBoundaries + totalTime;
453        this._xScaleFactor = overviewPane._overviewContainer.clientWidth / totalTime;
454    },
455
456    /**
457     * @param {number} time
458     * @return {number}
459     */
460    computePosition: function(time)
461    {
462        return (time - this._minimumBoundaries) * this._xScaleFactor;
463    },
464
465    /**
466     * @param {number} value
467     * @param {number=} precision
468     * @return {string}
469     */
470    formatTime: function(value, precision)
471    {
472        return Number.secondsToString((value - this._minimumBoundaries) / 1000);
473    },
474
475    /**
476     * @return {number}
477     */
478    maximumBoundary: function()
479    {
480        return this._maximumBoundaries;
481    },
482
483    /**
484     * @return {number}
485     */
486    minimumBoundary: function()
487    {
488        return this._minimumBoundaries;
489    },
490
491    /**
492     * @return {number}
493     */
494    zeroTime: function()
495    {
496        return this._minimumBoundaries;
497    },
498
499    /**
500     * @return {number}
501     */
502    boundarySpan: function()
503    {
504        return this._maximumBoundaries - this._minimumBoundaries;
505    }
506}
507
508/**
509 * @constructor
510 * @extends {WebInspector.VBox}
511 * @implements {WebInspector.FlameChartDelegate}
512 * @param {!WebInspector.FlameChartDataProvider} dataProvider
513 */
514WebInspector.CPUProfileFlameChart.OverviewPane = function(dataProvider)
515{
516    WebInspector.VBox.call(this);
517    this.element.classList.add("flame-chart-overview-pane");
518    this._overviewContainer = this.element.createChild("div", "overview-container");
519    this._overviewGrid = new WebInspector.OverviewGrid("flame-chart");
520    this._overviewGrid.element.classList.add("fill");
521    this._overviewCanvas = this._overviewContainer.createChild("canvas", "flame-chart-overview-canvas");
522    this._overviewContainer.appendChild(this._overviewGrid.element);
523    this._overviewCalculator = new WebInspector.CPUProfileFlameChart.OverviewCalculator();
524    this._dataProvider = dataProvider;
525    this._overviewGrid.addEventListener(WebInspector.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this);
526}
527
528WebInspector.CPUProfileFlameChart.OverviewPane.prototype = {
529    /**
530     * @param {number} windowStartTime
531     * @param {number} windowEndTime
532     */
533    requestWindowTimes: function(windowStartTime, windowEndTime)
534    {
535        this._selectRange(windowStartTime, windowEndTime);
536    },
537
538    /**
539     * @param {!number} timeLeft
540     * @param {!number} timeRight
541     */
542    _selectRange: function(timeLeft, timeRight)
543    {
544        var startTime = this._dataProvider.minimumBoundary();
545        var totalTime = this._dataProvider.totalTime();
546        this._overviewGrid.setWindow((timeLeft - startTime) / totalTime, (timeRight - startTime) / totalTime);
547    },
548
549    /**
550     * @param {!WebInspector.Event} event
551     */
552    _onWindowChanged: function(event)
553    {
554        var startTime = this._dataProvider.minimumBoundary();
555        var totalTime = this._dataProvider.totalTime();
556        var data = {
557            windowTimeLeft: startTime + this._overviewGrid.windowLeft() * totalTime,
558            windowTimeRight: startTime + this._overviewGrid.windowRight() * totalTime
559        };
560        this.dispatchEventToListeners(WebInspector.OverviewGrid.Events.WindowChanged, data);
561    },
562
563    /**
564     * @return {?WebInspector.FlameChart.TimelineData}
565     */
566    _timelineData: function()
567    {
568        return this._dataProvider.timelineData();
569    },
570
571    onResize: function()
572    {
573        this._scheduleUpdate();
574    },
575
576    _scheduleUpdate: function()
577    {
578        if (this._updateTimerId)
579            return;
580        this._updateTimerId = requestAnimationFrame(this.update.bind(this));
581    },
582
583    update: function()
584    {
585        this._updateTimerId = 0;
586        var timelineData = this._timelineData();
587        if (!timelineData)
588            return;
589        this._resetCanvas(this._overviewContainer.clientWidth, this._overviewContainer.clientHeight - WebInspector.FlameChart.DividersBarHeight);
590        this._overviewCalculator._updateBoundaries(this);
591        this._overviewGrid.updateDividers(this._overviewCalculator);
592        this._drawOverviewCanvas();
593    },
594
595    _drawOverviewCanvas: function()
596    {
597        var canvasWidth = this._overviewCanvas.width;
598        var canvasHeight = this._overviewCanvas.height;
599        var drawData = this._calculateDrawData(canvasWidth);
600        var context = this._overviewCanvas.getContext("2d");
601        var ratio = window.devicePixelRatio;
602        var offsetFromBottom = ratio;
603        var lineWidth = 1;
604        var yScaleFactor = canvasHeight / (this._dataProvider.maxStackDepth() * 1.1);
605        context.lineWidth = lineWidth;
606        context.translate(0.5, 0.5);
607        context.strokeStyle = "rgba(20,0,0,0.4)";
608        context.fillStyle = "rgba(214,225,254,0.8)";
609        context.moveTo(-lineWidth, canvasHeight + lineWidth);
610        context.lineTo(-lineWidth, Math.round(canvasHeight - drawData[0] * yScaleFactor - offsetFromBottom));
611        var value;
612        for (var x = 0; x < canvasWidth; ++x) {
613            value = Math.round(canvasHeight - drawData[x] * yScaleFactor - offsetFromBottom);
614            context.lineTo(x, value);
615        }
616        context.lineTo(canvasWidth + lineWidth, value);
617        context.lineTo(canvasWidth + lineWidth, canvasHeight + lineWidth);
618        context.fill();
619        context.stroke();
620        context.closePath();
621    },
622
623    /**
624     * @param {number} width
625     * @return {!Uint8Array}
626     */
627    _calculateDrawData: function(width)
628    {
629        var dataProvider = this._dataProvider;
630        var timelineData = this._timelineData();
631        var entryStartTimes = timelineData.entryStartTimes;
632        var entryTotalTimes = timelineData.entryTotalTimes;
633        var entryLevels = timelineData.entryLevels;
634        var length = entryStartTimes.length;
635        var minimumBoundary = this._dataProvider.minimumBoundary();
636
637        var drawData = new Uint8Array(width);
638        var scaleFactor = width / dataProvider.totalTime();
639
640        for (var entryIndex = 0; entryIndex < length; ++entryIndex) {
641            var start = Math.floor((entryStartTimes[entryIndex] - minimumBoundary) * scaleFactor);
642            var finish = Math.floor((entryStartTimes[entryIndex] - minimumBoundary + entryTotalTimes[entryIndex]) * scaleFactor);
643            for (var x = start; x <= finish; ++x)
644                drawData[x] = Math.max(drawData[x], entryLevels[entryIndex] + 1);
645        }
646        return drawData;
647    },
648
649    /**
650     * @param {!number} width
651     * @param {!number} height
652     */
653    _resetCanvas: function(width, height)
654    {
655        var ratio = window.devicePixelRatio;
656        this._overviewCanvas.width = width * ratio;
657        this._overviewCanvas.height = height * ratio;
658        this._overviewCanvas.style.width = width + "px";
659        this._overviewCanvas.style.height = height + "px";
660    },
661
662    __proto__: WebInspector.VBox.prototype
663}
664