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 */
36WebInspector.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
91WebInspector.TimelineOverviewPane.Mode = {
92    Events: "Events",
93    Frames: "Frames",
94    Memory: "Memory"
95};
96
97WebInspector.TimelineOverviewPane.Events = {
98    ModeChanged: "ModeChanged",
99    WindowChanged: "WindowChanged"
100};
101
102WebInspector.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 */
314WebInspector.TimelineOverviewCalculator = function()
315{
316}
317
318WebInspector.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 */
390WebInspector.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
400WebInspector.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 */
454WebInspector.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
463WebInspector.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 */
540WebInspector.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 */
558WebInspector.TimelineEventOverview._numberOfStrips = 3;
559
560/** @const */
561WebInspector.TimelineEventOverview._stripGradientHeight = 120;
562
563WebInspector.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 */
645WebInspector.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
664WebInspector.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 */
950WebInspector.TimelineWindowFilter = function(pane)
951{
952    this._pane = pane;
953}
954
955WebInspector.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