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