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         const targetFPS = 20;
    140         var result = 1000.0 / targetFPS;
    141         if (!frames.length)
    142             return result;
    143 
    144         var durations = [];
    145         for (var i = 0; i < frames.length; ++i) {
    146             if (frames[i])
    147                 durations.push(frames[i].duration);
    148         }
    149         var medianFrameLength = durations.qselect(Math.floor(durations.length / 2));
    150 
    151         // Optimize appearance for 30fps, but leave some space so it's evident when a frame overflows.
    152         // However, if at least half frames won't fit at this scale, fall back to using autoscale.
    153         if (result >= medianFrameLength)
    154             return result;
    155 
    156         var maxFrameLength = Math.max.apply(Math, durations);
    157         return Math.min(medianFrameLength * 2, maxFrameLength);
    158     },
    159 
    160     /**
    161      * @param {!Array.<!WebInspector.TimelineFrame>} frames
    162      * @param {number} scale
    163      * @param {number} windowHeight
    164      */
    165     _renderBars: function(frames, scale, windowHeight)
    166     {
    167         const maxPadding = 5 * window.devicePixelRatio;
    168         this._actualOuterBarWidth = Math.min((this._canvas.width - 2 * this._outerPadding) / frames.length, this._maxInnerBarWidth + maxPadding);
    169         this._actualPadding = Math.min(Math.floor(this._actualOuterBarWidth / 3), maxPadding);
    170 
    171         var barWidth = this._actualOuterBarWidth - this._actualPadding;
    172         for (var i = 0; i < frames.length; ++i) {
    173             if (frames[i])
    174                 this._renderBar(this._barNumberToScreenPosition(i), barWidth, windowHeight, frames[i], scale);
    175         }
    176     },
    177 
    178     /**
    179      * @param {number} n
    180      */
    181     _barNumberToScreenPosition: function(n)
    182     {
    183         return this._outerPadding + this._actualOuterBarWidth * n;
    184     },
    185 
    186     /**
    187      * @param {number} scale
    188      * @param {number} height
    189      */
    190     _drawFPSMarks: function(scale, height)
    191     {
    192         const fpsMarks = [30, 60];
    193 
    194         this._context.save();
    195         this._context.beginPath();
    196         this._context.font = (10 * window.devicePixelRatio) + "px " + window.getComputedStyle(this.element, null).getPropertyValue("font-family");
    197         this._context.textAlign = "right";
    198         this._context.textBaseline = "alphabetic";
    199 
    200         const labelPadding = 4 * window.devicePixelRatio;
    201         const baselineHeight = 3 * window.devicePixelRatio;
    202         var lineHeight = 12 * window.devicePixelRatio;
    203         var labelTopMargin = 0;
    204         var labelOffsetY = 0; // Labels are going to be under their grid lines.
    205 
    206         for (var i = 0; i < fpsMarks.length; ++i) {
    207             var fps = fpsMarks[i];
    208             // Draw lines one pixel above they need to be, so 60pfs line does not cross most of the frames tops.
    209             var y = height - Math.floor(1000.0 / fps * scale) - 0.5;
    210             var label = WebInspector.UIString("%d\u2009fps", fps);
    211             var labelWidth = this._context.measureText(label).width + 2 * labelPadding;
    212             var labelX = this._canvas.width;
    213 
    214             if (!i && labelTopMargin < y - lineHeight)
    215                 labelOffsetY = -lineHeight; // Labels are going to be over their grid lines.
    216             var labelY = y + labelOffsetY;
    217             if (labelY < labelTopMargin || labelY + lineHeight > height)
    218                 break; // No space for the label, so no line as well.
    219 
    220             this._context.moveTo(0, y);
    221             this._context.lineTo(this._canvas.width, y);
    222 
    223             this._context.fillStyle = "rgba(255, 255, 255, 0.5)";
    224             this._context.fillRect(labelX - labelWidth, labelY, labelWidth, lineHeight);
    225             this._context.fillStyle = "black";
    226             this._context.fillText(label, labelX - labelPadding, labelY + lineHeight - baselineHeight);
    227             labelTopMargin = labelY + lineHeight;
    228         }
    229         this._context.strokeStyle = "rgba(60, 60, 60, 0.4)";
    230         this._context.stroke();
    231         this._context.restore();
    232     },
    233 
    234     /**
    235      * @param {number} left
    236      * @param {number} width
    237      * @param {number} windowHeight
    238      * @param {!WebInspector.TimelineFrame} frame
    239      * @param {number} scale
    240      */
    241     _renderBar: function(left, width, windowHeight, frame, scale)
    242     {
    243         var categories = Object.keys(WebInspector.TimelineUIUtils.categories());
    244         var x = Math.floor(left) + 0.5;
    245         width = Math.floor(width);
    246 
    247         var totalCPUTime = frame.cpuTime;
    248         var normalizedScale = scale;
    249         if (totalCPUTime > frame.duration)
    250             normalizedScale *= frame.duration / totalCPUTime;
    251 
    252         for (var i = 0, bottomOffset = windowHeight; i < categories.length; ++i) {
    253             var category = categories[i];
    254             var duration = frame.timeByCategory[category];
    255             if (!duration)
    256                 continue;
    257             var height = Math.round(duration * normalizedScale);
    258             var y = Math.floor(bottomOffset - height) + 0.5;
    259 
    260             this._context.save();
    261             this._context.translate(x, 0);
    262             this._context.scale(width / this._maxInnerBarWidth, 1);
    263             this._context.fillStyle = this._fillStyles[category];
    264             this._context.fillRect(0, y, this._maxInnerBarWidth, Math.floor(height));
    265             this._context.strokeStyle = WebInspector.TimelineUIUtils.categories()[category].borderColor;
    266             this._context.beginPath();
    267             this._context.moveTo(0, y);
    268             this._context.lineTo(this._maxInnerBarWidth, y);
    269             this._context.stroke();
    270             this._context.restore();
    271 
    272             bottomOffset -= height;
    273         }
    274         // Draw a contour for the total frame time.
    275         var y0 = Math.floor(windowHeight - frame.duration * scale) + 0.5;
    276         var y1 = windowHeight + 0.5;
    277 
    278         this._context.strokeStyle = "rgba(90, 90, 90, 0.2)";
    279         this._context.beginPath();
    280         this._context.moveTo(x, y1);
    281         this._context.lineTo(x, y0);
    282         this._context.lineTo(x + width, y0);
    283         this._context.lineTo(x + width, y1);
    284         this._context.stroke();
    285     },
    286 
    287     /**
    288      * @param {number} windowLeft
    289      * @param {number} windowRight
    290      * @return {!{startTime: number, endTime: number}}
    291      */
    292     windowTimes: function(windowLeft, windowRight)
    293     {
    294         if (!this._barTimes.length)
    295             return WebInspector.TimelineOverviewBase.prototype.windowTimes.call(this, windowLeft, windowRight);
    296         var windowSpan = this._canvas.width;
    297         var leftOffset = windowLeft * windowSpan;
    298         var rightOffset = windowRight * windowSpan;
    299         var firstBar = Math.floor(Math.max(leftOffset - this._outerPadding + this._actualPadding, 0) / this._actualOuterBarWidth);
    300         var lastBar = Math.min(Math.floor(Math.max(rightOffset - this._outerPadding, 0)/ this._actualOuterBarWidth), this._barTimes.length - 1);
    301         if (firstBar >= this._barTimes.length)
    302             return {startTime: Infinity, endTime: Infinity};
    303 
    304         const snapTolerancePixels = 3;
    305         return {
    306             startTime: leftOffset > snapTolerancePixels ? this._barTimes[firstBar].startTime : this._model.minimumRecordTime(),
    307             endTime: (rightOffset + snapTolerancePixels > windowSpan) || (lastBar >= this._barTimes.length) ? this._model.maximumRecordTime() : this._barTimes[lastBar].endTime
    308         }
    309     },
    310 
    311     /**
    312      * @param {number} startTime
    313      * @param {number} endTime
    314      * @return {!{left: number, right: number}}
    315      */
    316     windowBoundaries: function(startTime, endTime)
    317     {
    318         if (this._barTimes.length === 0)
    319             return {left: 0, right: 1};
    320         /**
    321          * @param {number} time
    322          * @param {!{startTime:number, endTime:number}} barTime
    323          * @return {number}
    324          */
    325         function barStartComparator(time, barTime)
    326         {
    327             return time - barTime.startTime;
    328         }
    329         /**
    330          * @param {number} time
    331          * @param {!{startTime:number, endTime:number}} barTime
    332          * @return {number}
    333          */
    334         function barEndComparator(time, barTime)
    335         {
    336             // We need a frame where time is in [barTime.startTime, barTime.endTime), so exclude exact matches against endTime.
    337             if (time === barTime.endTime)
    338                 return 1;
    339             return time - barTime.endTime;
    340         }
    341         return {
    342             left: this._windowBoundaryFromTime(startTime, barEndComparator),
    343             right: this._windowBoundaryFromTime(endTime, barStartComparator)
    344         }
    345     },
    346 
    347     /**
    348      * @param {number} time
    349      * @param {function(number, !{startTime:number, endTime:number}):number} comparator
    350      */
    351     _windowBoundaryFromTime: function(time, comparator)
    352     {
    353         if (time === Infinity)
    354             return 1;
    355         var index = this._firstBarAfter(time, comparator);
    356         if (!index)
    357             return 0;
    358         return (this._barNumberToScreenPosition(index) - this._actualPadding / 2) / this._canvas.width;
    359     },
    360 
    361     /**
    362      * @param {number} time
    363      * @param {function(number, {startTime:number, endTime:number}):number} comparator
    364      */
    365     _firstBarAfter: function(time, comparator)
    366     {
    367         return insertionIndexForObjectInListSortedByFunction(time, this._barTimes, comparator);
    368     },
    369 
    370     __proto__: WebInspector.TimelineOverviewBase.prototype
    371 }
    372