Home | History | Annotate | Download | only in front_end
      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.View}
     34  * @param {WebInspector.TimelineModel} model
     35  */
     36 WebInspector.TimelineOverviewPane = function(model)
     37 {
     38     WebInspector.View.call(this);
     39     this.element.id = "timeline-overview-panel";
     40 
     41     this._windowStartTime = 0;
     42     this._windowEndTime = Infinity;
     43     this._eventDividers = [];
     44 
     45     this._model = model;
     46 
     47     this._topPaneSidebarElement = document.createElement("div");
     48     this._topPaneSidebarElement.id = "timeline-overview-sidebar";
     49 
     50     var overviewTreeElement = document.createElement("ol");
     51     overviewTreeElement.className = "sidebar-tree";
     52     this._topPaneSidebarElement.appendChild(overviewTreeElement);
     53     this.element.appendChild(this._topPaneSidebarElement);
     54 
     55     var topPaneSidebarTree = new TreeOutline(overviewTreeElement);
     56 
     57     this._overviewItems = {};
     58     this._overviewItems[WebInspector.TimelineOverviewPane.Mode.Events] = new WebInspector.SidebarTreeElement("timeline-overview-sidebar-events",
     59         WebInspector.UIString("Events"));
     60     this._overviewItems[WebInspector.TimelineOverviewPane.Mode.Frames] = new WebInspector.SidebarTreeElement("timeline-overview-sidebar-frames",
     61         WebInspector.UIString("Frames"));
     62     this._overviewItems[WebInspector.TimelineOverviewPane.Mode.Memory] = new WebInspector.SidebarTreeElement("timeline-overview-sidebar-memory",
     63         WebInspector.UIString("Memory"));
     64 
     65     for (var mode in this._overviewItems) {
     66         var item = this._overviewItems[mode];
     67         item.onselect = this.setMode.bind(this, mode);
     68         topPaneSidebarTree.appendChild(item);
     69     }
     70 
     71     this._overviewGrid = new WebInspector.OverviewGrid("timeline");
     72     this.element.appendChild(this._overviewGrid.element);
     73 
     74     var separatorElement = document.createElement("div");
     75     separatorElement.id = "timeline-overview-separator";
     76     this.element.appendChild(separatorElement);
     77 
     78     this._innerSetMode(WebInspector.TimelineOverviewPane.Mode.Events);
     79 
     80     var categories = WebInspector.TimelinePresentationModel.categories();
     81     for (var category in categories)
     82         categories[category].addEventListener(WebInspector.TimelineCategory.Events.VisibilityChanged, this._onCategoryVisibilityChanged, this);
     83 
     84     this._overviewCalculator = new WebInspector.TimelineOverviewCalculator();
     85 
     86     model.addEventListener(WebInspector.TimelineModel.Events.RecordAdded, this._onRecordAdded, this);
     87     model.addEventListener(WebInspector.TimelineModel.Events.RecordsCleared, this._reset, this);
     88     this._overviewGrid.addEventListener(WebInspector.OverviewGrid.Events.WindowChanged, this._onWindowChanged, this);
     89 }
     90 
     91 WebInspector.TimelineOverviewPane.Mode = {
     92     Events: "Events",
     93     Frames: "Frames",
     94     Memory: "Memory"
     95 };
     96 
     97 WebInspector.TimelineOverviewPane.Events = {
     98     ModeChanged: "ModeChanged",
     99     WindowChanged: "WindowChanged"
    100 };
    101 
    102 WebInspector.TimelineOverviewPane.prototype = {
    103     wasShown: function()
    104     {
    105         this._update();
    106     },
    107 
    108     onResize: function()
    109     {
    110         this._update();
    111     },
    112 
    113     setMode: function(newMode)
    114     {
    115         if (this._currentMode === newMode)
    116             return;
    117         var windowTimes;
    118         if (this._overviewControl)
    119             windowTimes = this._overviewControl.windowTimes(this.windowLeft(), this.windowRight());
    120         this._innerSetMode(newMode);
    121         this.dispatchEventToListeners(WebInspector.TimelineOverviewPane.Events.ModeChanged, this._currentMode);
    122         if (windowTimes && windowTimes.startTime >= 0)
    123             this.setWindowTimes(windowTimes.startTime, windowTimes.endTime);
    124         this._update();
    125     },
    126 
    127     _innerSetMode: function(newMode)
    128     {
    129         var windowTimes;
    130         if (this._overviewControl)
    131             this._overviewControl.detach();
    132         this._currentMode = newMode;
    133         this._overviewControl = this._createOverviewControl();
    134         this._overviewControl.show(this._overviewGrid.element);
    135         this._overviewItems[this._currentMode].revealAndSelect(false);
    136     },
    137 
    138     /**
    139      * @return {WebInspector.TimelineOverviewBase|null}
    140      */
    141     _createOverviewControl: function()
    142     {
    143         switch (this._currentMode) {
    144         case WebInspector.TimelineOverviewPane.Mode.Events:
    145             return new WebInspector.TimelineEventOverview(this._model);
    146         case WebInspector.TimelineOverviewPane.Mode.Frames:
    147             return new WebInspector.TimelineFrameOverview(this._model);
    148         case WebInspector.TimelineOverviewPane.Mode.Memory:
    149             return new WebInspector.TimelineMemoryOverview(this._model);
    150         }
    151         throw new Error("Invalid overview mode: " + this._currentMode);
    152     },
    153 
    154     _onCategoryVisibilityChanged: function(event)
    155     {
    156         this._overviewControl.categoryVisibilityChanged();
    157     },
    158 
    159     _update: function()
    160     {
    161         delete this._refreshTimeout;
    162 
    163         this._updateWindow();
    164         this._overviewCalculator.setWindow(this._model.minimumRecordTime(), this._model.maximumRecordTime());
    165         this._overviewCalculator.setDisplayWindow(0, this._overviewGrid.clientWidth());
    166 
    167         this._overviewControl.update();
    168         this._overviewGrid.updateDividers(this._overviewCalculator);
    169         this._updateEventDividers();
    170     },
    171 
    172     _updateEventDividers: function()
    173     {
    174         var records = this._eventDividers;
    175         this._overviewGrid.removeEventDividers();
    176         var dividers = [];
    177         for (var i = 0; i < records.length; ++i) {
    178             var record = records[i];
    179             var positions = this._overviewCalculator.computeBarGraphPercentages(record);
    180             var dividerPosition = Math.round(positions.start * 10);
    181             if (dividers[dividerPosition])
    182                 continue;
    183             var divider = WebInspector.TimelinePresentationModel.createEventDivider(record.type);
    184             divider.style.left = positions.start + "%";
    185             dividers[dividerPosition] = divider;
    186         }
    187         this._overviewGrid.addEventDividers(dividers);
    188     },
    189 
    190     /**
    191      * @param {number} width
    192      */
    193     sidebarResized: function(width)
    194     {
    195         this._overviewGrid.element.style.left = width + "px";
    196         this._topPaneSidebarElement.style.width = width + "px";
    197         this._update();
    198     },
    199 
    200     /**
    201      * @param {WebInspector.TimelineFrame} frame
    202      */
    203     addFrame: function(frame)
    204     {
    205         this._overviewControl.addFrame(frame);
    206         this._scheduleRefresh();
    207     },
    208 
    209     /**
    210      * @param {WebInspector.TimelineFrame} frame
    211      */
    212     zoomToFrame: function(frame)
    213     {
    214         var frameOverview = /** @type WebInspector.TimelineFrameOverview */ (this._overviewControl);
    215         var window = frameOverview.framePosition(frame);
    216         if (!window)
    217             return;
    218 
    219         this._overviewGrid.setWindowPosition(window.start, window.end);
    220     },
    221 
    222     _onRecordAdded: function(event)
    223     {
    224         var record = event.data;
    225         var eventDividers = this._eventDividers;
    226         function addEventDividers(record)
    227         {
    228             if (WebInspector.TimelinePresentationModel.isEventDivider(record))
    229                 eventDividers.push(record);
    230         }
    231         WebInspector.TimelinePresentationModel.forAllRecords([record], addEventDividers);
    232         this._scheduleRefresh();
    233     },
    234 
    235     _reset: function()
    236     {
    237         this._windowStartTime = 0;
    238         this._windowEndTime = Infinity;
    239         this._overviewCalculator.reset();
    240         this._overviewGrid.reset();
    241         this._overviewGrid.setResizeEnabled(false);
    242         this._eventDividers = [];
    243         this._overviewGrid.updateDividers(this._overviewCalculator);
    244         this._overviewControl.reset();
    245         this._update();
    246     },
    247 
    248     windowStartTime: function()
    249     {
    250         return this._windowStartTime || this._model.minimumRecordTime();
    251     },
    252 
    253     windowEndTime: function()
    254     {
    255         return this._windowEndTime < Infinity ? this._windowEndTime : this._model.maximumRecordTime();
    256     },
    257 
    258     windowLeft: function()
    259     {
    260         return this._overviewGrid.windowLeft();
    261     },
    262 
    263     windowRight: function()
    264     {
    265         return this._overviewGrid.windowRight();
    266     },
    267 
    268     _onWindowChanged: function()
    269     {
    270         if (this._ignoreWindowChangedEvent)
    271             return;
    272         var times = this._overviewControl.windowTimes(this.windowLeft(), this.windowRight());
    273         this._windowStartTime = times.startTime;
    274         this._windowEndTime = times.endTime;
    275         this.dispatchEventToListeners(WebInspector.TimelineOverviewPane.Events.WindowChanged);
    276     },
    277 
    278     /**
    279      * @param {Number} startTime
    280      * @param {Number} endTime
    281      */
    282     setWindowTimes: function(startTime, endTime)
    283     {
    284         this._windowStartTime = startTime;
    285         this._windowEndTime = endTime;
    286         this._updateWindow();
    287     },
    288 
    289     _updateWindow: function()
    290     {
    291         var windowBoundaries = this._overviewControl.windowBoundaries(this._windowStartTime, this._windowEndTime);
    292         this._ignoreWindowChangedEvent = true;
    293         this._overviewGrid.setWindow(windowBoundaries.left, windowBoundaries.right);
    294         this._overviewGrid.setResizeEnabled(this._model.records.length);
    295         this._ignoreWindowChangedEvent = false;
    296     },
    297 
    298     _scheduleRefresh: function()
    299     {
    300         if (this._refreshTimeout)
    301             return;
    302         if (!this.isShowing())
    303             return;
    304         this._refreshTimeout = setTimeout(this._update.bind(this), 300);
    305     },
    306 
    307     __proto__: WebInspector.View.prototype
    308 }
    309 
    310 /**
    311  * @constructor
    312  * @implements {WebInspector.TimelineGrid.Calculator}
    313  */
    314 WebInspector.TimelineOverviewCalculator = function()
    315 {
    316 }
    317 
    318 WebInspector.TimelineOverviewCalculator.prototype = {
    319     /**
    320      * @param {number} time
    321      */
    322     computePosition: function(time)
    323     {
    324         return (time - this._minimumBoundary) / this.boundarySpan() * this._workingArea + this.paddingLeft;
    325     },
    326 
    327     computeBarGraphPercentages: function(record)
    328     {
    329         var start = (WebInspector.TimelineModel.startTimeInSeconds(record) - this._minimumBoundary) / this.boundarySpan() * 100;
    330         var end = (WebInspector.TimelineModel.endTimeInSeconds(record) - this._minimumBoundary) / this.boundarySpan() * 100;
    331         return {start: start, end: end};
    332     },
    333 
    334     /**
    335      * @param {number=} minimum
    336      * @param {number=} maximum
    337      */
    338     setWindow: function(minimum, maximum)
    339     {
    340         this._minimumBoundary = minimum >= 0 ? minimum : undefined;
    341         this._maximumBoundary = maximum >= 0 ? maximum : undefined;
    342     },
    343 
    344     /**
    345      * @param {number} paddingLeft
    346      * @param {number} clientWidth
    347      */
    348     setDisplayWindow: function(paddingLeft, clientWidth)
    349     {
    350         this._workingArea = clientWidth - paddingLeft;
    351         this.paddingLeft = paddingLeft;
    352     },
    353 
    354     reset: function()
    355     {
    356         this.setWindow();
    357     },
    358 
    359     formatTime: function(value)
    360     {
    361         return Number.secondsToString(value);
    362     },
    363 
    364     maximumBoundary: function()
    365     {
    366         return this._maximumBoundary;
    367     },
    368 
    369     minimumBoundary: function()
    370     {
    371         return this._minimumBoundary;
    372     },
    373 
    374     zeroTime: function()
    375     {
    376         return this._minimumBoundary;
    377     },
    378 
    379     boundarySpan: function()
    380     {
    381         return this._maximumBoundary - this._minimumBoundary;
    382     }
    383 }
    384 
    385 /**
    386  * @constructor
    387  * @extends {WebInspector.View}
    388  * @param {WebInspector.TimelineModel} model
    389  */
    390 WebInspector.TimelineOverviewBase = function(model)
    391 {
    392     WebInspector.View.call(this);
    393     this.element.classList.add("fill");
    394 
    395     this._model = model;
    396     this._canvas = this.element.createChild("canvas", "fill");
    397     this._context = this._canvas.getContext("2d");
    398 }
    399 
    400 WebInspector.TimelineOverviewBase.prototype = {
    401     update: function() { },
    402     reset: function() { },
    403 
    404     categoryVisibilityChanged: function() { },
    405 
    406     /**
    407      * @param {WebInspector.TimelineFrame} frame
    408      */
    409     addFrame: function(frame) { },
    410 
    411     /**
    412      * @param {number} windowLeft
    413      * @param {number} windowRight
    414      */
    415     windowTimes: function(windowLeft, windowRight)
    416     {
    417         var absoluteMin = this._model.minimumRecordTime();
    418         var timeSpan = this._model.maximumRecordTime() - absoluteMin;
    419         return {
    420             startTime: absoluteMin + timeSpan * windowLeft,
    421             endTime: absoluteMin + timeSpan * windowRight
    422         };
    423     },
    424 
    425     /**
    426      * @param {number} startTime
    427      * @param {number} endTime
    428      */
    429     windowBoundaries: function(startTime, endTime)
    430     {
    431         var absoluteMin = this._model.minimumRecordTime();
    432         var timeSpan = this._model.maximumRecordTime() - absoluteMin;
    433         var haveRecords = absoluteMin >= 0;
    434         return {
    435             left: haveRecords && startTime ? Math.min((startTime - absoluteMin) / timeSpan, 1) : 0,
    436             right: haveRecords && endTime < Infinity ? (endTime - absoluteMin) / timeSpan : 1
    437         }
    438     },
    439 
    440     _resetCanvas: function()
    441     {
    442         this._canvas.width = this.element.clientWidth * window.devicePixelRatio;
    443         this._canvas.height = this.element.clientHeight * window.devicePixelRatio;
    444     },
    445 
    446     __proto__: WebInspector.View.prototype
    447 }
    448 
    449 /**
    450  * @constructor
    451  * @extends {WebInspector.TimelineOverviewBase}
    452  * @param {WebInspector.TimelineModel} model
    453  */
    454 WebInspector.TimelineMemoryOverview = function(model)
    455 {
    456     WebInspector.TimelineOverviewBase.call(this, model);
    457     this.element.id = "timeline-overview-memory";
    458 
    459     this._maxHeapSizeLabel = this.element.createChild("div", "max memory-graph-label");
    460     this._minHeapSizeLabel = this.element.createChild("div", "min memory-graph-label");
    461 }
    462 
    463 WebInspector.TimelineMemoryOverview.prototype = {
    464     update: function()
    465     {
    466         this._resetCanvas();
    467 
    468         var records = this._model.records;
    469         if (!records.length)
    470             return;
    471 
    472         const lowerOffset = 3;
    473         var maxUsedHeapSize = 0;
    474         var minUsedHeapSize = 100000000000;
    475         var minTime = this._model.minimumRecordTime();
    476         var maxTime = this._model.maximumRecordTime();
    477         WebInspector.TimelinePresentationModel.forAllRecords(records, function(r) {
    478             maxUsedHeapSize = Math.max(maxUsedHeapSize, r.usedHeapSize || maxUsedHeapSize);
    479             minUsedHeapSize = Math.min(minUsedHeapSize, r.usedHeapSize || minUsedHeapSize);
    480         });
    481         minUsedHeapSize = Math.min(minUsedHeapSize, maxUsedHeapSize);
    482 
    483         var width = this._canvas.width;
    484         var height = this._canvas.height - lowerOffset;
    485         var xFactor = width / (maxTime - minTime);
    486         var yFactor = height / Math.max(maxUsedHeapSize - minUsedHeapSize, 1);
    487 
    488         var histogram = new Array(width);
    489         WebInspector.TimelinePresentationModel.forAllRecords(records, function(r) {
    490             if (!r.usedHeapSize)
    491                 return;
    492             var x = Math.round((WebInspector.TimelineModel.endTimeInSeconds(r) - minTime) * xFactor);
    493             var y = Math.round((r.usedHeapSize - minUsedHeapSize) * yFactor);
    494             histogram[x] = Math.max(histogram[x] || 0, y);
    495         });
    496 
    497         height++; // +1 so that the border always fit into the canvas area.
    498 
    499         var y = 0;
    500         var isFirstPoint = true;
    501         var ctx = this._context;
    502         ctx.beginPath();
    503         ctx.moveTo(0, this._canvas.height);
    504         for (var x = 0; x < histogram.length; x++) {
    505             if (typeof histogram[x] === "undefined")
    506                 continue;
    507             if (isFirstPoint) {
    508                 isFirstPoint = false;
    509                 y = histogram[x];
    510                 ctx.lineTo(0, height - y);
    511             }
    512             ctx.lineTo(x, height - y);
    513             y = histogram[x];
    514             ctx.lineTo(x, height - y);
    515         }
    516         ctx.lineTo(width, height - y);
    517         ctx.lineTo(width, this._canvas.height);
    518         ctx.lineTo(0, this._canvas.height);
    519         ctx.closePath();
    520 
    521         ctx.lineWidth = 0.5;
    522         ctx.strokeStyle = "rgba(20,0,0,0.8)";
    523         ctx.stroke();
    524 
    525         ctx.fillStyle = "rgba(214,225,254, 0.8);";
    526         ctx.fill();
    527 
    528         this._maxHeapSizeLabel.textContent = Number.bytesToString(maxUsedHeapSize);
    529         this._minHeapSizeLabel.textContent = Number.bytesToString(minUsedHeapSize);
    530     },
    531 
    532     __proto__: WebInspector.TimelineOverviewBase.prototype
    533 }
    534 
    535 /**
    536  * @constructor
    537  * @extends {WebInspector.TimelineOverviewBase}
    538  * @param {WebInspector.TimelineModel} model
    539  */
    540 WebInspector.TimelineEventOverview = function(model)
    541 {
    542     WebInspector.TimelineOverviewBase.call(this, model);
    543 
    544     this.element.id = "timeline-overview-events";
    545 
    546     this._fillStyles = {};
    547     var categories = WebInspector.TimelinePresentationModel.categories();
    548     for (var category in categories)
    549         this._fillStyles[category] = WebInspector.TimelinePresentationModel.createFillStyleForCategory(this._context, 0, WebInspector.TimelineEventOverview._stripGradientHeight, categories[category]);
    550 
    551     this._disabledCategoryFillStyle = WebInspector.TimelinePresentationModel.createFillStyle(this._context, 0, WebInspector.TimelineEventOverview._stripGradientHeight,
    552         "rgb(218, 218, 218)", "rgb(170, 170, 170)", "rgb(143, 143, 143)");
    553 
    554     this._disabledCategoryBorderStyle = "rgb(143, 143, 143)";
    555 }
    556 
    557 /** @const */
    558 WebInspector.TimelineEventOverview._numberOfStrips = 3;
    559 
    560 /** @const */
    561 WebInspector.TimelineEventOverview._stripGradientHeight = 120;
    562 
    563 WebInspector.TimelineEventOverview.prototype = {
    564     update: function()
    565     {
    566         this._resetCanvas();
    567 
    568         var stripHeight = Math.round(this._canvas.height  / WebInspector.TimelineEventOverview._numberOfStrips);
    569         var timeOffset = this._model.minimumRecordTime();
    570         var timeSpan = this._model.maximumRecordTime() - timeOffset;
    571         var scale = this._canvas.width / timeSpan;
    572 
    573         var lastBarByGroup = [];
    574 
    575         this._context.fillStyle = "rgba(0, 0, 0, 0.05)";
    576         for (var i = 1; i < WebInspector.TimelineEventOverview._numberOfStrips; i += 2)
    577             this._context.fillRect(0.5, i * stripHeight + 0.5, this._canvas.width, stripHeight);
    578 
    579         function appendRecord(record)
    580         {
    581             if (record.type === WebInspector.TimelineModel.RecordType.BeginFrame)
    582                 return;
    583             var recordStart = Math.floor((WebInspector.TimelineModel.startTimeInSeconds(record) - timeOffset) * scale);
    584             var recordEnd = Math.ceil((WebInspector.TimelineModel.endTimeInSeconds(record) - timeOffset) * scale);
    585             var category = WebInspector.TimelinePresentationModel.categoryForRecord(record);
    586             if (category.overviewStripGroupIndex < 0)
    587                 return;
    588             var bar = lastBarByGroup[category.overviewStripGroupIndex];
    589             // This bar may be merged with previous -- so just adjust the previous bar.
    590             const barsMergeThreshold = 2;
    591             if (bar && bar.category === category && bar.end + barsMergeThreshold >= recordStart) {
    592                 if (recordEnd > bar.end)
    593                     bar.end = recordEnd;
    594                 return;
    595             }
    596             if (bar)
    597                 this._renderBar(bar.start, bar.end, stripHeight, bar.category);
    598             lastBarByGroup[category.overviewStripGroupIndex] = { start: recordStart, end: recordEnd, category: category };
    599         }
    600         WebInspector.TimelinePresentationModel.forAllRecords(this._model.records, appendRecord.bind(this));
    601         for (var i = 0; i < lastBarByGroup.length; ++i) {
    602             if (lastBarByGroup[i])
    603                 this._renderBar(lastBarByGroup[i].start, lastBarByGroup[i].end, stripHeight, lastBarByGroup[i].category);
    604         }
    605     },
    606 
    607     categoryVisibilityChanged: function()
    608     {
    609         this.update();
    610     },
    611 
    612     /**
    613      * @param {number} begin
    614      * @param {number} end
    615      * @param {number} height
    616      * @param {WebInspector.TimelineCategory} category
    617      */
    618     _renderBar: function(begin, end, height, category)
    619     {
    620         const stripPadding = 4 * window.devicePixelRatio;
    621         const innerStripHeight = height - 2 * stripPadding;
    622 
    623         var x = begin + 0.5;
    624         var y = category.overviewStripGroupIndex * height + stripPadding + 0.5;
    625         var width = Math.max(end - begin, 1);
    626 
    627         this._context.save();
    628         this._context.translate(x, y);
    629         this._context.scale(1, innerStripHeight / WebInspector.TimelineEventOverview._stripGradientHeight);
    630         this._context.fillStyle = category.hidden ? this._disabledCategoryFillStyle : this._fillStyles[category.name];
    631         this._context.fillRect(0, 0, width, WebInspector.TimelineEventOverview._stripGradientHeight);
    632         this._context.strokeStyle = category.hidden ? this._disabledCategoryBorderStyle : category.borderColor;
    633         this._context.strokeRect(0, 0, width, WebInspector.TimelineEventOverview._stripGradientHeight);
    634         this._context.restore();
    635     },
    636 
    637     __proto__: WebInspector.TimelineOverviewBase.prototype
    638 }
    639 
    640 /**
    641  * @constructor
    642  * @extends {WebInspector.TimelineOverviewBase}
    643  * @param {WebInspector.TimelineModel} model
    644  */
    645 WebInspector.TimelineFrameOverview = function(model)
    646 {
    647     WebInspector.TimelineOverviewBase.call(this, model);
    648     this.element.id = "timeline-overview-frames";
    649     this.reset();
    650 
    651     this._outerPadding = 4 * window.devicePixelRatio;
    652     this._maxInnerBarWidth = 10 * window.devicePixelRatio;
    653 
    654     // The below two are really computed by update() -- but let's have something so that windowTimes() is happy.
    655     this._actualPadding = 5 * window.devicePixelRatio;
    656     this._actualOuterBarWidth = this._maxInnerBarWidth + this._actualPadding;
    657 
    658     this._fillStyles = {};
    659     var categories = WebInspector.TimelinePresentationModel.categories();
    660     for (var category in categories)
    661         this._fillStyles[category] = WebInspector.TimelinePresentationModel.createFillStyleForCategory(this._context, this._maxInnerBarWidth, 0, categories[category]);
    662 }
    663 
    664 WebInspector.TimelineFrameOverview.prototype = {
    665     reset: function()
    666     {
    667         this._recordsPerBar = 1;
    668         /** @type {!Array.<{startTime:number, endTime:number}>} */
    669         this._barTimes = [];
    670         this._frames = [];
    671     },
    672 
    673     update: function()
    674     {
    675         const minBarWidth = 4 * window.devicePixelRatio;
    676         this._resetCanvas();
    677         this._framesPerBar = Math.max(1, this._frames.length * minBarWidth / this._canvas.width);
    678         this._barTimes = [];
    679         var visibleFrames = this._aggregateFrames(this._framesPerBar);
    680 
    681         const paddingTop = 4 * window.devicePixelRatio;
    682 
    683         // Optimize appearance for 30fps. However, if at least half frames won't fit at this scale,
    684         // fall back to using autoscale.
    685         const targetFPS = 30;
    686         var fullBarLength = 1.0 / targetFPS;
    687         if (fullBarLength < this._medianFrameLength)
    688             fullBarLength = Math.min(this._medianFrameLength * 2, this._maxFrameLength);
    689 
    690         var scale = (this._canvas.height - paddingTop) / fullBarLength;
    691         this._renderBars(visibleFrames, scale);
    692     },
    693 
    694     /**
    695      * @param {WebInspector.TimelineFrame} frame
    696      */
    697     addFrame: function(frame)
    698     {
    699         this._frames.push(frame);
    700     },
    701 
    702     framePosition: function(frame)
    703     {
    704         var frameNumber = this._frames.indexOf(frame);
    705         if (frameNumber < 0)
    706             return;
    707         var barNumber = Math.floor(frameNumber / this._framesPerBar);
    708         var firstBar = this._framesPerBar > 1 ? barNumber : Math.max(barNumber - 1, 0);
    709         var lastBar = this._framesPerBar > 1 ? barNumber : Math.min(barNumber + 1, this._barTimes.length - 1);
    710         return {
    711             start: Math.ceil(this._barNumberToScreenPosition(firstBar) - this._actualPadding / 2),
    712             end: Math.floor(this._barNumberToScreenPosition(lastBar + 1) - this._actualPadding / 2)
    713         }
    714     },
    715 
    716     /**
    717      * @param {number} framesPerBar
    718      */
    719     _aggregateFrames: function(framesPerBar)
    720     {
    721         var visibleFrames = [];
    722         var durations = [];
    723 
    724         this._maxFrameLength = 0;
    725 
    726         for (var barNumber = 0, currentFrame = 0; currentFrame < this._frames.length; ++barNumber) {
    727             var barStartTime = this._frames[currentFrame].startTime;
    728             var longestFrame = null;
    729 
    730             for (var lastFrame = Math.min(Math.floor((barNumber + 1) * framesPerBar), this._frames.length);
    731                  currentFrame < lastFrame; ++currentFrame) {
    732                 if (!longestFrame || longestFrame.duration < this._frames[currentFrame].duration)
    733                     longestFrame = this._frames[currentFrame];
    734             }
    735             var barEndTime = this._frames[currentFrame - 1].endTime;
    736             if (longestFrame) {
    737                 this._maxFrameLength = Math.max(this._maxFrameLength, longestFrame.duration);
    738                 visibleFrames.push(longestFrame);
    739                 this._barTimes.push({ startTime: barStartTime, endTime: barEndTime });
    740                 durations.push(longestFrame.duration);
    741             }
    742         }
    743         this._medianFrameLength = durations.qselect(Math.floor(durations.length / 2));
    744         return visibleFrames;
    745     },
    746 
    747     /**
    748      * @param {Array.<WebInspector.TimelineFrame>} frames
    749      * @param {number} scale
    750      */
    751     _renderBars: function(frames, scale)
    752     {
    753         const maxPadding = 5 * window.devicePixelRatio;
    754         this._actualOuterBarWidth = Math.min((this._canvas.width - 2 * this._outerPadding) / frames.length, this._maxInnerBarWidth + maxPadding);
    755         this._actualPadding = Math.min(Math.floor(this._actualOuterBarWidth / 3), maxPadding);
    756 
    757         var barWidth = this._actualOuterBarWidth - this._actualPadding;
    758         for (var i = 0; i < frames.length; ++i)
    759             this._renderBar(this._barNumberToScreenPosition(i), barWidth, frames[i], scale);
    760 
    761         this._drawFPSMarks(scale);
    762     },
    763 
    764     /**
    765      * @param {number} n
    766      */
    767     _barNumberToScreenPosition: function(n)
    768     {
    769         return this._outerPadding + this._actualOuterBarWidth * n;
    770     },
    771 
    772     /**
    773      * @param {number} scale
    774      */
    775     _drawFPSMarks: function(scale)
    776     {
    777         const fpsMarks = [30, 60];
    778 
    779         this._context.save();
    780         this._context.beginPath();
    781         this._context.font = (10 * window.devicePixelRatio) + "px " + window.getComputedStyle(this.element, null).getPropertyValue("font-family");
    782         this._context.textAlign = "right";
    783         this._context.textBaseline = "alphabetic";
    784 
    785         const labelPadding = 4 * window.devicePixelRatio;
    786         const baselineHeight = 3 * window.devicePixelRatio;
    787         var lineHeight = 12 * window.devicePixelRatio;
    788         var labelTopMargin = 0;
    789         var labelOffsetY = 0; // Labels are going to be under their grid lines.
    790 
    791         for (var i = 0; i < fpsMarks.length; ++i) {
    792             var fps = fpsMarks[i];
    793             // Draw lines one pixel above they need to be, so 60pfs line does not cross most of the frames tops.
    794             var y = this._canvas.height - Math.floor(1.0 / fps * scale) - 0.5;
    795             var label = WebInspector.UIString("%d\u2009fps", fps);
    796             var labelWidth = this._context.measureText(label).width + 2 * labelPadding;
    797             var labelX = this._canvas.width;
    798 
    799             if (!i && labelTopMargin < y - lineHeight)
    800                 labelOffsetY = -lineHeight; // Labels are going to be over their grid lines.
    801             var labelY = y + labelOffsetY;
    802             if (labelY < labelTopMargin || labelY + lineHeight > this._canvas.height)
    803                 break; // No space for the label, so no line as well.
    804 
    805             this._context.moveTo(0, y);
    806             this._context.lineTo(this._canvas.width, y);
    807 
    808             this._context.fillStyle = "rgba(255, 255, 255, 0.5)";
    809             this._context.fillRect(labelX - labelWidth, labelY, labelWidth, lineHeight);
    810             this._context.fillStyle = "black";
    811             this._context.fillText(label, labelX - labelPadding, labelY + lineHeight - baselineHeight);
    812             labelTopMargin = labelY + lineHeight;
    813         }
    814         this._context.strokeStyle = "rgba(128, 128, 128, 0.5)";
    815         this._context.stroke();
    816         this._context.restore();
    817     },
    818 
    819     _renderBar: function(left, width, frame, scale)
    820     {
    821         var categories = Object.keys(WebInspector.TimelinePresentationModel.categories());
    822         if (!categories.length)
    823             return;
    824         var x = Math.floor(left) + 0.5;
    825         width = Math.floor(width);
    826 
    827         for (var i = 0, bottomOffset = this._canvas.height; i < categories.length; ++i) {
    828             var category = categories[i];
    829             var duration = frame.timeByCategory[category];
    830 
    831             if (!duration)
    832                 continue;
    833             var height = duration * scale;
    834             var y = Math.floor(bottomOffset - height) + 0.5;
    835 
    836             this._context.save();
    837             this._context.translate(x, 0);
    838             this._context.scale(width / this._maxInnerBarWidth, 1);
    839             this._context.fillStyle = this._fillStyles[category];
    840             this._context.fillRect(0, y, this._maxInnerBarWidth, Math.floor(height));
    841             this._context.strokeStyle = WebInspector.TimelinePresentationModel.categories()[category].borderColor;
    842             this._context.beginPath();
    843             this._context.moveTo(0, y);
    844             this._context.lineTo(this._maxInnerBarWidth, y);
    845             this._context.stroke();
    846             this._context.restore();
    847 
    848             bottomOffset -= height - 1;
    849         }
    850         // Draw a contour for the total frame time.
    851         var y0 = Math.floor(this._canvas.height - frame.duration * scale) + 0.5;
    852         var y1 = this._canvas.height + 0.5;
    853 
    854         this._context.strokeStyle = "rgba(90, 90, 90, 0.3)";
    855         this._context.beginPath();
    856         this._context.moveTo(x, y1);
    857         this._context.lineTo(x, y0);
    858         this._context.lineTo(x + width, y0);
    859         this._context.lineTo(x + width, y1);
    860         this._context.stroke();
    861     },
    862 
    863     /**
    864      * @param {number} windowLeft
    865      * @param {number} windowRight
    866      */
    867     windowTimes: function(windowLeft, windowRight)
    868     {
    869         if (!this._barTimes.length)
    870             return WebInspector.TimelineOverviewBase.prototype.windowTimes.call(this, windowLeft, windowRight);
    871         var windowSpan = this._canvas.width;
    872         var leftOffset = windowLeft * windowSpan - this._outerPadding + this._actualPadding;
    873         var rightOffset = windowRight * windowSpan - this._outerPadding;
    874         var firstBar = Math.floor(Math.max(leftOffset, 0) / this._actualOuterBarWidth);
    875         var lastBar = Math.min(Math.floor(rightOffset / this._actualOuterBarWidth), this._barTimes.length - 1);
    876         if (firstBar >= this._barTimes.length)
    877             return {startTime: Infinity, endTime: Infinity};
    878 
    879         const snapToRightTolerancePixels = 3;
    880         return {
    881             startTime: this._barTimes[firstBar].startTime,
    882             endTime: (rightOffset + snapToRightTolerancePixels > windowSpan) || (lastBar >= this._barTimes.length) ? Infinity : this._barTimes[lastBar].endTime
    883         }
    884     },
    885 
    886     /**
    887      * @param {number} startTime
    888      * @param {number} endTime
    889      */
    890     windowBoundaries: function(startTime, endTime)
    891     {
    892         /**
    893          * @param {number} time
    894          * @param {{startTime:number, endTime:number}} barTime
    895          * @return {number}
    896          */
    897         function barStartComparator(time, barTime)
    898         {
    899             return time - barTime.startTime;
    900         }
    901         /**
    902          * @param {number} time
    903          * @param {{startTime:number, endTime:number}} barTime
    904          * @return {number}
    905          */
    906         function barEndComparator(time, barTime)
    907         {
    908             // We need a frame where time is in [barTime.startTime, barTime.endTime), so exclude exact matches against endTime.
    909             if (time === barTime.endTime)
    910                 return 1;
    911             return time - barTime.endTime;
    912         }
    913         return {
    914             left: this._windowBoundaryFromTime(startTime, barEndComparator),
    915             right: this._windowBoundaryFromTime(endTime, barStartComparator)
    916         }
    917     },
    918 
    919     /**
    920      * @param {number} time
    921      * @param {function(number, {startTime:number, endTime:number}):number} comparator
    922      */
    923     _windowBoundaryFromTime: function(time, comparator)
    924     {
    925         if (time === Infinity)
    926             return 1;
    927         var index = this._firstBarAfter(time, comparator);
    928         if (!index)
    929             return 0;
    930         return (this._barNumberToScreenPosition(index) - this._actualPadding / 2) / this._canvas.width;
    931     },
    932 
    933     /**
    934      * @param {number} time
    935      * @param {function(number, {startTime:number, endTime:number}):number} comparator
    936      */
    937     _firstBarAfter: function(time, comparator)
    938     {
    939         return insertionIndexForObjectInListSortedByFunction(time, this._barTimes, comparator);
    940     },
    941 
    942     __proto__: WebInspector.TimelineOverviewBase.prototype
    943 }
    944 
    945 /**
    946  * @param {WebInspector.TimelineOverviewPane} pane
    947  * @constructor
    948  * @implements {WebInspector.TimelinePresentationModel.Filter}
    949  */
    950 WebInspector.TimelineWindowFilter = function(pane)
    951 {
    952     this._pane = pane;
    953 }
    954 
    955 WebInspector.TimelineWindowFilter.prototype = {
    956     /**
    957      * @param {!WebInspector.TimelinePresentationModel.Record} record
    958      * @return {boolean}
    959      */
    960     accept: function(record)
    961     {
    962         return record.lastChildEndTime >= this._pane._windowStartTime && record.startTime <= this._pane._windowEndTime;
    963     }
    964 }
    965