Home | History | Annotate | Download | only in profiler
      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  */
     38 WebInspector.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 
     46 WebInspector.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  */
    355 WebInspector.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  */
    376 WebInspector.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 
    391 WebInspector.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  */
    432 WebInspector.CPUProfileFlameChart.OverviewCalculator = function()
    433 {
    434 }
    435 
    436 WebInspector.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  */
    514 WebInspector.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 
    528 WebInspector.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