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      * @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  */
    342 WebInspector.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  */
    363 WebInspector.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 
    378 WebInspector.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  */
    419 WebInspector.CPUProfileFlameChart.OverviewCalculator = function()
    420 {
    421 }
    422 
    423 WebInspector.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  */
    501 WebInspector.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 
    515 WebInspector.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