Home | History | Annotate | Download | only in timeline
      1 /*
      2  * Copyright (C) 2013 Google Inc. All rights reserved.
      3  *
      4  * Redistribution and use in source and binary forms, with or without
      5  * modification, are permitted provided that the following conditions are
      6  * met:
      7  *
      8  *     * Redistributions of source code must retain the above copyright
      9  * notice, this list of conditions and the following disclaimer.
     10  *     * Redistributions in binary form must reproduce the above
     11  * copyright notice, this list of conditions and the following disclaimer
     12  * in the documentation and/or other materials provided with the
     13  * distribution.
     14  *     * Neither the name of Google Inc. nor the names of its
     15  * contributors may be used to endorse or promote products derived from
     16  * this software without specific prior written permission.
     17  *
     18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     19  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     21  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     22  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     23  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     24  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     25  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     26  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     28  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     29  */
     30 
     31 /**
     32  * @constructor
     33  * @extends {WebInspector.TimelineOverviewBase}
     34  * @param {!WebInspector.TimelineModel} model
     35  * @param {!WebInspector.TimelineFrameModelBase} frameModel
     36  */
     37 WebInspector.TimelineFrameOverview = function(model, frameModel)
     38 {
     39     WebInspector.TimelineOverviewBase.call(this, model);
     40     this.element.id = "timeline-overview-frames";
     41     this._frameModel = frameModel;
     42     this.reset();
     43 
     44     this._outerPadding = 4 * window.devicePixelRatio;
     45     this._maxInnerBarWidth = 10 * window.devicePixelRatio;
     46     this._topPadding = 6 * window.devicePixelRatio;
     47 
     48     // The below two are really computed by update() -- but let's have something so that windowTimes() is happy.
     49     this._actualPadding = 5 * window.devicePixelRatio;
     50     this._actualOuterBarWidth = this._maxInnerBarWidth + this._actualPadding;
     51 
     52     this._fillStyles = {};
     53     var categories = WebInspector.TimelineUIUtils.categories();
     54     for (var category in categories)
     55         this._fillStyles[category] = WebInspector.TimelineUIUtils.createFillStyleForCategory(this._context, this._maxInnerBarWidth, 0, categories[category]);
     56 
     57     this._frameTopShadeGradient = this._context.createLinearGradient(0, 0, 0, this._topPadding);
     58     this._frameTopShadeGradient.addColorStop(0, "rgba(255, 255, 255, 0.9)");
     59     this._frameTopShadeGradient.addColorStop(1, "rgba(255, 255, 255, 0.2)");
     60 }
     61 
     62 WebInspector.TimelineFrameOverview.prototype = {
     63     /**
     64      * @param {!WebInspector.OverviewGrid} grid
     65      */
     66     setOverviewGrid: function(grid)
     67     {
     68         this._overviewGrid = grid;
     69         this._overviewGrid.element.classList.add("timeline-overview-frames-mode");
     70     },
     71 
     72     dispose: function()
     73     {
     74         this._overviewGrid.element.classList.remove("timeline-overview-frames-mode");
     75     },
     76 
     77     reset: function()
     78     {
     79         this._recordsPerBar = 1;
     80         /** @type {!Array.<!{startTime:number, endTime:number}>} */
     81         this._barTimes = [];
     82     },
     83 
     84     update: function()
     85     {
     86         this.resetCanvas();
     87         this._barTimes = [];
     88 
     89         const minBarWidth = 4 * window.devicePixelRatio;
     90         var frames = this._frameModel.frames();
     91         var framesPerBar = Math.max(1, frames.length * minBarWidth / this._canvas.width);
     92         var visibleFrames = this._aggregateFrames(frames, framesPerBar);
     93 
     94         this._context.save();
     95         var scale = (this._canvas.height - this._topPadding) / this._computeTargetFrameLength(visibleFrames);
     96         this._renderBars(visibleFrames, scale, this._canvas.height);
     97         this._context.fillStyle = this._frameTopShadeGradient;
     98         this._context.fillRect(0, 0, this._canvas.width, this._topPadding);
     99         this._drawFPSMarks(scale, this._canvas.height);
    100         this._context.restore();
    101     },
    102 
    103     /**
    104      * @param {!Array.<!WebInspector.TimelineFrame>} frames
    105      * @param {number} framesPerBar
    106      * @return {!Array.<!WebInspector.TimelineFrame>}
    107      */
    108     _aggregateFrames: function(frames, framesPerBar)
    109     {
    110         var visibleFrames = [];
    111         for (var barNumber = 0, currentFrame = 0; currentFrame < frames.length; ++barNumber) {
    112             var barStartTime = frames[currentFrame].startTime;
    113             var longestFrame = null;
    114             var longestDuration = 0;
    115 
    116             for (var lastFrame = Math.min(Math.floor((barNumber + 1) * framesPerBar), frames.length);
    117                  currentFrame < lastFrame; ++currentFrame) {
    118                 var duration = frames[currentFrame].duration;
    119                 if (!longestFrame || longestDuration < duration) {
    120                     longestFrame = frames[currentFrame];
    121                     longestDuration = duration;
    122                 }
    123             }
    124             var barEndTime = frames[currentFrame - 1].endTime;
    125             if (longestFrame) {
    126                 visibleFrames.push(longestFrame);
    127                 this._barTimes.push({ startTime: barStartTime, endTime: barEndTime });
    128             }
    129         }
    130         return visibleFrames;
    131     },
    132 
    133     /**
    134      * @param {!Array.<!WebInspector.TimelineFrame>} frames
    135      * @return {number}
    136      */
    137     _computeTargetFrameLength: function(frames)
    138     {
    139         var durations = [];
    140         for (var i = 0; i < frames.length; ++i) {
    141             if (frames[i])
    142                 durations.push(frames[i].duration);
    143         }
    144         var medianFrameLength = durations.qselect(Math.floor(durations.length / 2));
    145 
    146         // Optimize appearance for 30fps, but leave some space so it's evident when a frame overflows.
    147         // However, if at least half frames won't fit at this scale, fall back to using autoscale.
    148         const targetFPS = 20;
    149         var result = 1000.0 / targetFPS;
    150         if (result >= medianFrameLength)
    151             return result;
    152 
    153         var maxFrameLength = Math.max.apply(Math, durations);
    154         return Math.min(medianFrameLength * 2, maxFrameLength);
    155     },
    156 
    157     /**
    158      * @param {!Array.<!WebInspector.TimelineFrame>} frames
    159      * @param {number} scale
    160      * @param {number} windowHeight
    161      */
    162     _renderBars: function(frames, scale, windowHeight)
    163     {
    164         const maxPadding = 5 * window.devicePixelRatio;
    165         this._actualOuterBarWidth = Math.min((this._canvas.width - 2 * this._outerPadding) / frames.length, this._maxInnerBarWidth + maxPadding);
    166         this._actualPadding = Math.min(Math.floor(this._actualOuterBarWidth / 3), maxPadding);
    167 
    168         var barWidth = this._actualOuterBarWidth - this._actualPadding;
    169         for (var i = 0; i < frames.length; ++i) {
    170             if (frames[i])
    171                 this._renderBar(this._barNumberToScreenPosition(i), barWidth, windowHeight, frames[i], scale);
    172         }
    173     },
    174 
    175     /**
    176      * @param {number} n
    177      */
    178     _barNumberToScreenPosition: function(n)
    179     {
    180         return this._outerPadding + this._actualOuterBarWidth * n;
    181     },
    182 
    183     /**
    184      * @param {number} scale
    185      * @param {number} height
    186      */
    187     _drawFPSMarks: function(scale, height)
    188     {
    189         const fpsMarks = [30, 60];
    190 
    191         this._context.save();
    192         this._context.beginPath();
    193         this._context.font = (10 * window.devicePixelRatio) + "px " + window.getComputedStyle(this.element, null).getPropertyValue("font-family");
    194         this._context.textAlign = "right";
    195         this._context.textBaseline = "alphabetic";
    196 
    197         const labelPadding = 4 * window.devicePixelRatio;
    198         const baselineHeight = 3 * window.devicePixelRatio;
    199         var lineHeight = 12 * window.devicePixelRatio;
    200         var labelTopMargin = 0;
    201         var labelOffsetY = 0; // Labels are going to be under their grid lines.
    202 
    203         for (var i = 0; i < fpsMarks.length; ++i) {
    204             var fps = fpsMarks[i];
    205             // Draw lines one pixel above they need to be, so 60pfs line does not cross most of the frames tops.
    206             var y = height - Math.floor(1000.0 / fps * scale) - 0.5;
    207             var label = WebInspector.UIString("%d\u2009fps", fps);
    208             var labelWidth = this._context.measureText(label).width + 2 * labelPadding;
    209             var labelX = this._canvas.width;
    210 
    211             if (!i && labelTopMargin < y - lineHeight)
    212                 labelOffsetY = -lineHeight; // Labels are going to be over their grid lines.
    213             var labelY = y + labelOffsetY;
    214             if (labelY < labelTopMargin || labelY + lineHeight > height)
    215                 break; // No space for the label, so no line as well.
    216 
    217             this._context.moveTo(0, y);
    218             this._context.lineTo(this._canvas.width, y);
    219 
    220             this._context.fillStyle = "rgba(255, 255, 255, 0.5)";
    221             this._context.fillRect(labelX - labelWidth, labelY, labelWidth, lineHeight);
    222             this._context.fillStyle = "black";
    223             this._context.fillText(label, labelX - labelPadding, labelY + lineHeight - baselineHeight);
    224             labelTopMargin = labelY + lineHeight;
    225         }
    226         this._context.strokeStyle = "rgba(60, 60, 60, 0.4)";
    227         this._context.stroke();
    228         this._context.restore();
    229     },
    230 
    231     /**
    232      * @param {number} left
    233      * @param {number} width
    234      * @param {number} windowHeight
    235      * @param {!WebInspector.TimelineFrame} frame
    236      * @param {number} scale
    237      */
    238     _renderBar: function(left, width, windowHeight, frame, scale)
    239     {
    240         var categories = Object.keys(WebInspector.TimelineUIUtils.categories());
    241         var x = Math.floor(left) + 0.5;
    242         width = Math.floor(width);
    243 
    244         var totalCPUTime = frame.cpuTime;
    245         var normalizedScale = scale;
    246         if (totalCPUTime > frame.duration)
    247             normalizedScale *= frame.duration / totalCPUTime;
    248 
    249         for (var i = 0, bottomOffset = windowHeight; i < categories.length; ++i) {
    250             var category = categories[i];
    251             var duration = frame.timeByCategory[category];
    252             if (!duration)
    253                 continue;
    254             var height = Math.round(duration * normalizedScale);
    255             var y = Math.floor(bottomOffset - height) + 0.5;
    256 
    257             this._context.save();
    258             this._context.translate(x, 0);
    259             this._context.scale(width / this._maxInnerBarWidth, 1);
    260             this._context.fillStyle = this._fillStyles[category];
    261             this._context.fillRect(0, y, this._maxInnerBarWidth, Math.floor(height));
    262             this._context.strokeStyle = WebInspector.TimelineUIUtils.categories()[category].borderColor;
    263             this._context.beginPath();
    264             this._context.moveTo(0, y);
    265             this._context.lineTo(this._maxInnerBarWidth, y);
    266             this._context.stroke();
    267             this._context.restore();
    268 
    269             bottomOffset -= height;
    270         }
    271         // Draw a contour for the total frame time.
    272         var y0 = Math.floor(windowHeight - frame.duration * scale) + 0.5;
    273         var y1 = windowHeight + 0.5;
    274 
    275         this._context.strokeStyle = "rgba(90, 90, 90, 0.3)";
    276         this._context.beginPath();
    277         this._context.moveTo(x, y1);
    278         this._context.lineTo(x, y0);
    279         this._context.lineTo(x + width, y0);
    280         this._context.lineTo(x + width, y1);
    281         this._context.stroke();
    282     },
    283 
    284     /**
    285      * @param {number} windowLeft
    286      * @param {number} windowRight
    287      * @return {!{startTime: number, endTime: number}}
    288      */
    289     windowTimes: function(windowLeft, windowRight)
    290     {
    291         if (!this._barTimes.length)
    292             return WebInspector.TimelineOverviewBase.prototype.windowTimes.call(this, windowLeft, windowRight);
    293         var windowSpan = this._canvas.width;
    294         var leftOffset = windowLeft * windowSpan;
    295         var rightOffset = windowRight * windowSpan;
    296         var firstBar = Math.floor(Math.max(leftOffset - this._outerPadding + this._actualPadding, 0) / this._actualOuterBarWidth);
    297         var lastBar = Math.min(Math.floor(Math.max(rightOffset - this._outerPadding, 0)/ this._actualOuterBarWidth), this._barTimes.length - 1);
    298         if (firstBar >= this._barTimes.length)
    299             return {startTime: Infinity, endTime: Infinity};
    300 
    301         const snapTolerancePixels = 3;
    302         return {
    303             startTime: leftOffset > snapTolerancePixels ? this._barTimes[firstBar].startTime : this._model.minimumRecordTime(),
    304             endTime: (rightOffset + snapTolerancePixels > windowSpan) || (lastBar >= this._barTimes.length) ? this._model.maximumRecordTime() : this._barTimes[lastBar].endTime
    305         }
    306     },
    307 
    308     /**
    309      * @param {number} startTime
    310      * @param {number} endTime
    311      * @return {!{left: number, right: number}}
    312      */
    313     windowBoundaries: function(startTime, endTime)
    314     {
    315         if (this._barTimes.length === 0)
    316             return {left: 0, right: 1};
    317         /**
    318          * @param {number} time
    319          * @param {!{startTime:number, endTime:number}} barTime
    320          * @return {number}
    321          */
    322         function barStartComparator(time, barTime)
    323         {
    324             return time - barTime.startTime;
    325         }
    326         /**
    327          * @param {number} time
    328          * @param {!{startTime:number, endTime:number}} barTime
    329          * @return {number}
    330          */
    331         function barEndComparator(time, barTime)
    332         {
    333             // We need a frame where time is in [barTime.startTime, barTime.endTime), so exclude exact matches against endTime.
    334             if (time === barTime.endTime)
    335                 return 1;
    336             return time - barTime.endTime;
    337         }
    338         return {
    339             left: this._windowBoundaryFromTime(startTime, barEndComparator),
    340             right: this._windowBoundaryFromTime(endTime, barStartComparator)
    341         }
    342     },
    343 
    344     /**
    345      * @param {number} time
    346      * @param {function(number, !{startTime:number, endTime:number}):number} comparator
    347      */
    348     _windowBoundaryFromTime: function(time, comparator)
    349     {
    350         if (time === Infinity)
    351             return 1;
    352         var index = this._firstBarAfter(time, comparator);
    353         if (!index)
    354             return 0;
    355         return (this._barNumberToScreenPosition(index) - this._actualPadding / 2) / this._canvas.width;
    356     },
    357 
    358     /**
    359      * @param {number} time
    360      * @param {function(number, {startTime:number, endTime:number}):number} comparator
    361      */
    362     _firstBarAfter: function(time, comparator)
    363     {
    364         return insertionIndexForObjectInListSortedByFunction(time, this._barTimes, comparator);
    365     },
    366 
    367     __proto__: WebInspector.TimelineOverviewBase.prototype
    368 }
    369