Web Inspector: Timelines: can't reliably stop/start a recording
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / TimelineOverview.js
1 /*
2  * Copyright (C) 2013, 2015-2016 Apple 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
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WI.TimelineOverview = class TimelineOverview extends WI.View
27 {
28     constructor(timelineRecording)
29     {
30         super();
31
32         console.assert(timelineRecording instanceof WI.TimelineRecording);
33
34         this._timelinesViewModeSettings = this._createViewModeSettings(WI.TimelineOverview.ViewMode.Timelines, WI.TimelineOverview.MinimumDurationPerPixel, WI.TimelineOverview.MaximumDurationPerPixel, 0.01, 0, 15);
35         this._instrumentTypes = WI.TimelineManager.availableTimelineTypes();
36
37         if (WI.FPSInstrument.supported()) {
38             let minimumDurationPerPixel = 1 / WI.TimelineRecordFrame.MaximumWidthPixels;
39             let maximumDurationPerPixel = 1 / WI.TimelineRecordFrame.MinimumWidthPixels;
40             this._renderingFramesViewModeSettings = this._createViewModeSettings(WI.TimelineOverview.ViewMode.RenderingFrames, minimumDurationPerPixel, maximumDurationPerPixel, minimumDurationPerPixel, 0, 100);
41         }
42
43         this._recording = timelineRecording;
44         this._recording.addEventListener(WI.TimelineRecording.Event.InstrumentAdded, this._instrumentAdded, this);
45         this._recording.addEventListener(WI.TimelineRecording.Event.InstrumentRemoved, this._instrumentRemoved, this);
46         this._recording.addEventListener(WI.TimelineRecording.Event.MarkerAdded, this._markerAdded, this);
47         this._recording.addEventListener(WI.TimelineRecording.Event.Reset, this._recordingReset, this);
48
49         this.element.classList.add("timeline-overview");
50         this._updateWheelAndGestureHandlers();
51
52         this._graphsContainerView = new WI.View;
53         this._graphsContainerView.element.classList.add("graphs-container");
54         this._graphsContainerView.element.addEventListener("click", this._handleGraphsContainerClick.bind(this));
55         this.addSubview(this._graphsContainerView);
56
57         this._selectedTimelineRecord = null;
58         this._overviewGraphsByTypeMap = new Map;
59
60         this._editInstrumentsButton = new WI.ActivateButtonNavigationItem("toggle-edit-instruments", WI.UIString("Edit configuration"), WI.UIString("Save configuration"));
61         this._editInstrumentsButton.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._toggleEditingInstruments, this);
62         this._editingInstruments = false;
63         this._updateEditInstrumentsButton();
64
65         let instrumentsNavigationBar = new WI.NavigationBar;
66         instrumentsNavigationBar.element.classList.add("timelines");
67         instrumentsNavigationBar.addNavigationItem(new WI.FlexibleSpaceNavigationItem);
68         instrumentsNavigationBar.addNavigationItem(this._editInstrumentsButton);
69         this.addSubview(instrumentsNavigationBar);
70
71         this._timelinesTreeOutline = new WI.TreeOutline;
72         this._timelinesTreeOutline.element.classList.add("timelines");
73         this._timelinesTreeOutline.disclosureButtons = false;
74         this._timelinesTreeOutline.large = true;
75         this._timelinesTreeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._timelinesTreeSelectionDidChange, this);
76         this.element.appendChild(this._timelinesTreeOutline.element);
77
78         this._treeElementsByTypeMap = new Map;
79
80         this._timelineRuler = new WI.TimelineRuler;
81         this._timelineRuler.allowsClippedLabels = true;
82         this._timelineRuler.allowsTimeRangeSelection = true;
83         this._timelineRuler.element.addEventListener("mousedown", this._timelineRulerMouseDown.bind(this));
84         this._timelineRuler.element.addEventListener("click", this._timelineRulerMouseClicked.bind(this));
85         this._timelineRuler.addEventListener(WI.TimelineRuler.Event.TimeRangeSelectionChanged, this._timeRangeSelectionChanged, this);
86         this.addSubview(this._timelineRuler);
87
88         this._currentTimeMarker = new WI.TimelineMarker(0, WI.TimelineMarker.Type.CurrentTime);
89         this._timelineRuler.addMarker(this._currentTimeMarker);
90
91         this._scrollContainerElement = document.createElement("div");
92         this._scrollContainerElement.classList.add("scroll-container");
93         this._scrollContainerElement.addEventListener("scroll", this._handleScrollEvent.bind(this));
94         this.element.appendChild(this._scrollContainerElement);
95
96         this._scrollWidthSizer = document.createElement("div");
97         this._scrollWidthSizer.classList.add("scroll-width-sizer");
98         this._scrollContainerElement.appendChild(this._scrollWidthSizer);
99
100         this._startTime = 0;
101         this._currentTime = 0;
102         this._revealCurrentTime = false;
103         this._endTime = 0;
104         this._pixelAlignDuration = false;
105         this._mouseWheelDelta = 0;
106         this._cachedScrollContainerWidth = NaN;
107         this._timelineRulerSelectionChanged = false;
108         this._viewMode = WI.TimelineOverview.ViewMode.Timelines;
109         this._selectedTimeline = null;
110
111         for (let instrument of this._recording.instruments)
112             this._instrumentAdded(instrument);
113
114         if (!WI.timelineManager.isCapturingPageReload())
115             this._resetSelection();
116
117         this._viewModeDidChange();
118
119         WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStateChanged, this._handleTimelineCapturingStateChanged, this);
120         WI.timelineManager.addEventListener(WI.TimelineManager.Event.RecordingImported, this._recordingImported, this);
121     }
122
123     // Import / Export
124
125     exportData()
126     {
127         let json = {
128             secondsPerPixel: this.secondsPerPixel,
129             scrollStartTime: this.scrollStartTime,
130             selectionStartTime: this.selectionStartTime,
131             selectionDuration: this.selectionDuration,
132         };
133
134         if (this._selectedTimeline)
135             json.selectedTimelineType = this._selectedTimeline.type;
136
137         return json;
138     }
139
140     // Public
141
142     get selectedTimeline()
143     {
144         return this._selectedTimeline;
145     }
146
147     set selectedTimeline(x)
148     {
149         if (this._editingInstruments)
150             return;
151
152         if (this._selectedTimeline === x)
153             return;
154
155         this._selectedTimeline = x;
156         if (this._selectedTimeline) {
157             let treeElement = this._treeElementsByTypeMap.get(this._selectedTimeline.type);
158             console.assert(treeElement, "Missing tree element for timeline", this._selectedTimeline);
159
160             let omitFocus = true;
161             let wasSelectedByUser = false;
162             treeElement.select(omitFocus, wasSelectedByUser);
163         } else if (this._timelinesTreeOutline.selectedTreeElement)
164             this._timelinesTreeOutline.selectedTreeElement.deselect();
165     }
166
167     get editingInstruments()
168     {
169         return this._editingInstruments;
170     }
171
172     get viewMode()
173     {
174         return this._viewMode;
175     }
176
177     set viewMode(x)
178     {
179         if (this._editingInstruments)
180             return;
181
182         if (this._viewMode === x)
183             return;
184
185         this._viewMode = x;
186         this._viewModeDidChange();
187     }
188
189     get startTime()
190     {
191         return this._startTime;
192     }
193
194     set startTime(x)
195     {
196         x = x || 0;
197
198         if (this._startTime === x)
199             return;
200
201         if (this._viewMode !== WI.TimelineOverview.ViewMode.RenderingFrames) {
202             let selectionOffset = this.selectionStartTime - this._startTime;
203             this.selectionStartTime = selectionOffset + x;
204         }
205
206         this._startTime = x;
207
208         this.needsLayout();
209     }
210
211     get currentTime()
212     {
213         return this._currentTime;
214     }
215
216     set currentTime(x)
217     {
218         x = x || 0;
219
220         if (this._currentTime === x)
221             return;
222
223         this._currentTime = x;
224         this._revealCurrentTime = true;
225
226         this.needsLayout();
227     }
228
229     get secondsPerPixel()
230     {
231         return this._currentSettings.durationPerPixelSetting.value;
232     }
233
234     set secondsPerPixel(x)
235     {
236         x = Math.min(this._currentSettings.maximumDurationPerPixel, Math.max(this._currentSettings.minimumDurationPerPixel, x));
237
238         if (this.secondsPerPixel === x)
239             return;
240
241         if (this._pixelAlignDuration) {
242             x = 1 / Math.round(1 / x);
243             if (this.secondsPerPixel === x)
244                 return;
245         }
246
247         this._currentSettings.durationPerPixelSetting.value = x;
248
249         this.needsLayout();
250     }
251
252     get pixelAlignDuration()
253     {
254         return this._pixelAlignDuration;
255     }
256
257     set pixelAlignDuration(x)
258     {
259         if (this._pixelAlignDuration === x)
260             return;
261
262         this._mouseWheelDelta = 0;
263         this._pixelAlignDuration = x;
264         if (this._pixelAlignDuration)
265             this.secondsPerPixel = 1 / Math.round(1 / this.secondsPerPixel);
266     }
267
268     get endTime()
269     {
270         return this._endTime;
271     }
272
273     set endTime(x)
274     {
275         x = x || 0;
276
277         if (this._endTime === x)
278             return;
279
280         this._endTime = x;
281
282         this.needsLayout();
283     }
284
285     get scrollStartTime()
286     {
287         return this._currentSettings.scrollStartTime;
288     }
289
290     set scrollStartTime(x)
291     {
292         x = x || 0;
293
294         if (this.scrollStartTime === x)
295             return;
296
297         this._currentSettings.scrollStartTime = x;
298
299         this.needsLayout();
300     }
301
302     get scrollContainerWidth()
303     {
304         return this._cachedScrollContainerWidth;
305     }
306
307     get visibleDuration()
308     {
309         if (isNaN(this._cachedScrollContainerWidth)) {
310             this._cachedScrollContainerWidth = this._scrollContainerElement.offsetWidth;
311             if (!this._cachedScrollContainerWidth)
312                 this._cachedScrollContainerWidth = NaN;
313         }
314
315         return this._cachedScrollContainerWidth * this.secondsPerPixel;
316     }
317
318     get selectionStartTime()
319     {
320         return this._timelineRuler.selectionStartTime;
321     }
322
323     set selectionStartTime(x)
324     {
325         x = x || 0;
326
327         if (this._timelineRuler.selectionStartTime === x)
328             return;
329
330         let selectionDuration = this.selectionDuration;
331         this._timelineRuler.selectionStartTime = x;
332         this._timelineRuler.selectionEndTime = x + selectionDuration;
333     }
334
335     get selectionDuration()
336     {
337         return this._timelineRuler.selectionEndTime - this._timelineRuler.selectionStartTime;
338     }
339
340     set selectionDuration(x)
341     {
342         x = Math.max(this._timelineRuler.minimumSelectionDuration, x);
343
344         this._timelineRuler.selectionEndTime = this._timelineRuler.selectionStartTime + x;
345     }
346
347     get height()
348     {
349         let height = 0;
350         for (let overviewGraph of this._overviewGraphsByTypeMap.values()) {
351             if (overviewGraph.visible)
352                 height += overviewGraph.height;
353         }
354         return height;
355     }
356
357     get visible()
358     {
359         return this._visible;
360     }
361
362     shown()
363     {
364         this._visible = true;
365
366         for (let [type, overviewGraph] of this._overviewGraphsByTypeMap) {
367             if (this._canShowTimelineType(type))
368                 overviewGraph.shown();
369         }
370
371         this.updateLayout(WI.View.LayoutReason.Resize);
372     }
373
374     hidden()
375     {
376         this._visible = false;
377
378         for (let overviewGraph of this._overviewGraphsByTypeMap.values())
379             overviewGraph.hidden();
380
381         this.hideScanner();
382     }
383
384     closed()
385     {
386         WI.timelineManager.removeEventListener(null, null, this);
387
388         super.closed();
389     }
390
391     reset()
392     {
393         this._selectedTimelineRecord = null;
394         for (let overviewGraph of this._overviewGraphsByTypeMap.values())
395             overviewGraph.reset();
396
397         this._mouseWheelDelta = 0;
398
399         this._resetSelection();
400     }
401
402     revealMarker(marker)
403     {
404         this.scrollStartTime = marker.time - (this.visibleDuration / 2);
405     }
406
407     recordWasFiltered(timeline, record, filtered)
408     {
409         let overviewGraph = this._overviewGraphsByTypeMap.get(timeline.type);
410         console.assert(overviewGraph, "Missing overview graph for timeline type " + timeline.type);
411         if (!overviewGraph)
412             return;
413
414         console.assert(overviewGraph.visible, "Record filtered in hidden overview graph", record);
415
416         overviewGraph.recordWasFiltered(record, filtered);
417     }
418
419     selectRecord(timeline, record)
420     {
421         let overviewGraph = this._overviewGraphsByTypeMap.get(timeline.type);
422         console.assert(overviewGraph, "Missing overview graph for timeline type " + timeline.type);
423         if (!overviewGraph)
424             return;
425
426         console.assert(overviewGraph.visible, "Record selected in hidden overview graph", record);
427
428         overviewGraph.selectedRecord = record;
429     }
430
431     showScanner(time)
432     {
433         this._timelineRuler.showScanner(time);
434     }
435
436     hideScanner()
437     {
438         this._timelineRuler.hideScanner();
439     }
440
441     updateLayoutIfNeeded(layoutReason)
442     {
443         if (this.layoutPending) {
444             super.updateLayoutIfNeeded(layoutReason);
445             return;
446         }
447
448         this._timelineRuler.updateLayoutIfNeeded(layoutReason);
449
450         for (let overviewGraph of this._overviewGraphsByTypeMap.values()) {
451             if (overviewGraph.visible)
452                 overviewGraph.updateLayoutIfNeeded(layoutReason);
453         }
454     }
455
456     discontinuitiesInTimeRange(startTime, endTime)
457     {
458         return this._recording.discontinuitiesInTimeRange(startTime, endTime);
459     }
460
461     // Protected
462
463     get timelineRuler()
464     {
465         return this._timelineRuler;
466     }
467
468     layout()
469     {
470         let startTime = this._startTime;
471         let endTime = this._endTime;
472         let currentTime = this._currentTime;
473         if (this._viewMode === WI.TimelineOverview.ViewMode.RenderingFrames) {
474             let renderingFramesTimeline = this._recording.timelines.get(WI.TimelineRecord.Type.RenderingFrame);
475             console.assert(renderingFramesTimeline, "Recoring missing rendering frames timeline");
476
477             startTime = 0;
478             endTime = renderingFramesTimeline.records.length;
479             currentTime = endTime;
480         }
481
482         // Calculate the required width based on the duration and seconds per pixel.
483         let duration = endTime - startTime;
484         let newWidth = Math.ceil(duration / this.secondsPerPixel);
485
486         // Update all relevant elements to the new required width.
487         this._updateElementWidth(this._scrollWidthSizer, newWidth);
488
489         this._currentTimeMarker.time = currentTime;
490
491         if (this._revealCurrentTime) {
492             this.revealMarker(this._currentTimeMarker);
493             this._revealCurrentTime = false;
494         }
495
496         const visibleDuration = this.visibleDuration;
497
498         // Clamp the scroll start time to match what the scroll bar would allow.
499         let scrollStartTime = Math.min(this.scrollStartTime, endTime - visibleDuration);
500         scrollStartTime = Math.max(startTime, scrollStartTime);
501
502         this._timelineRuler.zeroTime = startTime;
503         this._timelineRuler.startTime = scrollStartTime;
504         this._timelineRuler.secondsPerPixel = this.secondsPerPixel;
505
506         if (!this._dontUpdateScrollLeft) {
507             this._ignoreNextScrollEvent = true;
508             let scrollLeft = Math.ceil((scrollStartTime - startTime) / this.secondsPerPixel);
509             if (scrollLeft)
510                 this._scrollContainerElement.scrollLeft = scrollLeft;
511         }
512
513         for (let overviewGraph of this._overviewGraphsByTypeMap.values()) {
514             if (!overviewGraph.visible)
515                 continue;
516
517             overviewGraph.zeroTime = startTime;
518             overviewGraph.startTime = scrollStartTime;
519             overviewGraph.currentTime = currentTime;
520             overviewGraph.endTime = scrollStartTime + visibleDuration;
521         }
522     }
523
524     sizeDidChange()
525     {
526         this._cachedScrollContainerWidth = NaN;
527     }
528
529     // Private
530
531     _updateElementWidth(element, newWidth)
532     {
533         var currentWidth = parseInt(element.style.width);
534         if (currentWidth !== newWidth)
535             element.style.width = newWidth + "px";
536     }
537
538     _handleScrollEvent(event)
539     {
540         if (this._ignoreNextScrollEvent) {
541             this._ignoreNextScrollEvent = false;
542             return;
543         }
544
545         this._dontUpdateScrollLeft = true;
546
547         let scrollOffset = this._scrollContainerElement.scrollLeft;
548         if (WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL)
549             this.scrollStartTime = this._startTime - (scrollOffset * this.secondsPerPixel);
550         else
551             this.scrollStartTime = this._startTime + (scrollOffset * this.secondsPerPixel);
552
553         // Force layout so we can update with the scroll position synchronously.
554         this.updateLayoutIfNeeded();
555
556         this._dontUpdateScrollLeft = false;
557
558         this.element.classList.toggle("has-scrollbar", this._scrollContainerElement.clientHeight <= 1);
559     }
560
561     _handleWheelEvent(event)
562     {
563         // Ignore cloned events that come our way, we already handled the original.
564         if (event.__cloned)
565             return;
566
567         // Ignore wheel events while handing gestures.
568         if (this._handlingGesture)
569             return;
570
571         // Require twice the vertical delta to overcome horizontal scrolling. This prevents most
572         // cases of inadvertent zooming for slightly diagonal scrolls.
573         if (Math.abs(event.deltaX) >= Math.abs(event.deltaY) * 0.5) {
574             // Clone the event to dispatch it on the scroll container. Mark it as cloned so we don't get into a loop.
575             let newWheelEvent = new event.constructor(event.type, event);
576             newWheelEvent.__cloned = true;
577
578             this._scrollContainerElement.dispatchEvent(newWheelEvent);
579             return;
580         }
581
582         // Remember the mouse position in time.
583         let mouseOffset = event.pageX - this._graphsContainerView.element.totalOffsetLeft;
584         let mousePositionTime = this._currentSettings.scrollStartTime + (mouseOffset * this.secondsPerPixel);
585         let deviceDirection = event.webkitDirectionInvertedFromDevice ? 1 : -1;
586         let delta = event.deltaY * (this.secondsPerPixel / WI.TimelineOverview.ScrollDeltaDenominator) * deviceDirection;
587
588         // Reset accumulated wheel delta when direction changes.
589         if (this._pixelAlignDuration && (delta < 0 && this._mouseWheelDelta >= 0 || delta >= 0 && this._mouseWheelDelta < 0))
590             this._mouseWheelDelta = 0;
591
592         let previousDurationPerPixel = this.secondsPerPixel;
593         this._mouseWheelDelta += delta;
594         this.secondsPerPixel += this._mouseWheelDelta;
595
596         if (this.secondsPerPixel === this._currentSettings.minimumDurationPerPixel && delta < 0 || this.secondsPerPixel === this._currentSettings.maximumDurationPerPixel && delta >= 0)
597             this._mouseWheelDelta = 0;
598         else
599             this._mouseWheelDelta = previousDurationPerPixel + this._mouseWheelDelta - this.secondsPerPixel;
600
601         // Center the zoom around the mouse based on the remembered mouse position time.
602         this.scrollStartTime = mousePositionTime - (mouseOffset * this.secondsPerPixel);
603
604         this.element.classList.toggle("has-scrollbar", this._scrollContainerElement.clientHeight <= 1);
605
606         event.preventDefault();
607         event.stopPropagation();
608     }
609
610     _handleGestureStart(event)
611     {
612         if (this._handlingGesture) {
613             // FIXME: <https://webkit.org/b/151068> [Mac] Unexpected gesturestart events when already handling gesture
614             return;
615         }
616
617         let mouseOffset = event.pageX - this._graphsContainerView.element.totalOffsetLeft;
618         let mousePositionTime = this._currentSettings.scrollStartTime + (mouseOffset * this.secondsPerPixel);
619
620         this._handlingGesture = true;
621         this._gestureStartStartTime = mousePositionTime;
622         this._gestureStartDurationPerPixel = this.secondsPerPixel;
623
624         this.element.classList.toggle("has-scrollbar", this._scrollContainerElement.clientHeight <= 1);
625
626         event.preventDefault();
627         event.stopPropagation();
628     }
629
630     _handleGestureChange(event)
631     {
632         // Cap zooming out at 5x.
633         let scale = Math.max(1 / 5, event.scale);
634
635         let mouseOffset = event.pageX - this._graphsContainerView.element.totalOffsetLeft;
636         let newSecondsPerPixel = this._gestureStartDurationPerPixel / scale;
637
638         this.secondsPerPixel = newSecondsPerPixel;
639         this.scrollStartTime = this._gestureStartStartTime - (mouseOffset * this.secondsPerPixel);
640
641         event.preventDefault();
642         event.stopPropagation();
643     }
644
645     _handleGestureEnd(event)
646     {
647         this._handlingGesture = false;
648         this._gestureStartStartTime = NaN;
649         this._gestureStartDurationPerPixel = NaN;
650     }
651
652     _instrumentAdded(instrumentOrEvent)
653     {
654         let instrument = instrumentOrEvent instanceof WI.Instrument ? instrumentOrEvent : instrumentOrEvent.data.instrument;
655         console.assert(instrument instanceof WI.Instrument, instrument);
656
657         let timeline = this._recording.timelineForInstrument(instrument);
658         console.assert(!this._overviewGraphsByTypeMap.has(timeline.type), timeline);
659         console.assert(!this._treeElementsByTypeMap.has(timeline.type), timeline);
660
661         let treeElement = new WI.TimelineTreeElement(timeline);
662         let insertionIndex = insertionIndexForObjectInListSortedByFunction(treeElement, this._timelinesTreeOutline.children, this._compareTimelineTreeElements.bind(this));
663         this._timelinesTreeOutline.insertChild(treeElement, insertionIndex);
664         this._treeElementsByTypeMap.set(timeline.type, treeElement);
665
666         let overviewGraph = WI.TimelineOverviewGraph.createForTimeline(timeline, this);
667         overviewGraph.addEventListener(WI.TimelineOverviewGraph.Event.RecordSelected, this._handleOverviewGraphRecordSelected, this);
668         this._overviewGraphsByTypeMap.set(timeline.type, overviewGraph);
669         this._graphsContainerView.insertSubviewBefore(overviewGraph, this._graphsContainerView.subviews[insertionIndex]);
670
671         treeElement.element.style.height = overviewGraph.height + "px";
672
673         if (!this._canShowTimelineType(timeline.type)) {
674             overviewGraph.hidden();
675             treeElement.hidden = true;
676         }
677
678         this.needsLayout();
679     }
680
681     _instrumentRemoved(event)
682     {
683         let instrument = event.data.instrument;
684         console.assert(instrument instanceof WI.Instrument, instrument);
685
686         let timeline = this._recording.timelineForInstrument(instrument);
687         let overviewGraph = this._overviewGraphsByTypeMap.get(timeline.type);
688         console.assert(overviewGraph, "Missing overview graph for timeline type", timeline.type);
689
690         let treeElement = this._treeElementsByTypeMap.get(timeline.type);
691         let shouldSuppressOnDeselect = false;
692         let shouldSuppressSelectSibling = true;
693         this._timelinesTreeOutline.removeChild(treeElement, shouldSuppressOnDeselect, shouldSuppressSelectSibling);
694
695         overviewGraph.removeEventListener(WI.TimelineOverviewGraph.Event.RecordSelected, this._handleOverviewGraphRecordSelected, this);
696         this._graphsContainerView.removeSubview(overviewGraph);
697
698         this._overviewGraphsByTypeMap.delete(timeline.type);
699         this._treeElementsByTypeMap.delete(timeline.type);
700     }
701
702     _markerAdded(event)
703     {
704         this._timelineRuler.addMarker(event.data.marker);
705     }
706
707     _handleGraphsContainerClick(event)
708     {
709         // Set when a WI.TimelineRecordBar receives the "click" first and is about to be selected.
710         if (event.__timelineRecordClickEventHandled)
711             return;
712
713         this._recordSelected(null, null);
714     }
715
716     _timelineRulerMouseDown(event)
717     {
718         this._timelineRulerSelectionChanged = false;
719     }
720
721     _timelineRulerMouseClicked(event)
722     {
723         if (this._timelineRulerSelectionChanged)
724             return;
725
726         for (let overviewGraph of this._overviewGraphsByTypeMap.values()) {
727             if (!overviewGraph.visible)
728                 continue;
729
730             let graphRect = overviewGraph.element.getBoundingClientRect();
731             if (!(event.pageX >= graphRect.left && event.pageX <= graphRect.right && event.pageY >= graphRect.top && event.pageY <= graphRect.bottom))
732                 continue;
733
734             // Clone the event to dispatch it on the overview graph element.
735             let newClickEvent = new event.constructor(event.type, event);
736             overviewGraph.element.dispatchEvent(newClickEvent);
737             return;
738         }
739     }
740
741     _timeRangeSelectionChanged(event)
742     {
743         this._timelineRulerSelectionChanged = true;
744
745         let startTime = this._viewMode === WI.TimelineOverview.ViewMode.Timelines ? this._startTime : 0;
746         this._currentSettings.selectionStartValueSetting.value = this.selectionStartTime - startTime;
747         this._currentSettings.selectionDurationSetting.value = this.selectionDuration;
748
749         this.dispatchEventToListeners(WI.TimelineOverview.Event.TimeRangeSelectionChanged);
750     }
751
752     _handleOverviewGraphRecordSelected(event)
753     {
754         let {record, recordBar} = event.data;
755
756         // Ignore deselection events, as they are handled by the newly selected record's timeline.
757         if (!record)
758             return;
759
760         this._recordSelected(record, recordBar);
761     }
762
763     _recordSelected(record, recordBar)
764     {
765         if (record === this._selectedTimelineRecord)
766             return;
767
768         if (this._selectedTimelineRecord && (!record || this._selectedTimelineRecord.type !== record.type)) {
769             let timelineOverviewGraph = this._overviewGraphsByTypeMap.get(this._selectedTimelineRecord.type);
770             console.assert(timelineOverviewGraph);
771             if (timelineOverviewGraph)
772                 timelineOverviewGraph.selectedRecord = null;
773         }
774
775         this._selectedTimelineRecord = record;
776
777         if (this._selectedTimelineRecord) {
778             let firstRecord = this._selectedTimelineRecord;
779             let lastRecord = this._selectedTimelineRecord;
780             if (recordBar) {
781                 firstRecord = recordBar.records[0];
782                 lastRecord = recordBar.records.lastValue;
783             }
784
785             let startTime = firstRecord instanceof WI.RenderingFrameTimelineRecord ? firstRecord.frameIndex : firstRecord.startTime;
786             let endTime = lastRecord instanceof WI.RenderingFrameTimelineRecord ? lastRecord.frameIndex : lastRecord.endTime;
787
788             if (firstRecord instanceof WI.CPUTimelineRecord) {
789                 let selectionPadding = WI.CPUTimelineOverviewGraph.samplingRatePerSecond * 2.25;
790                 this.selectionStartTime = startTime - selectionPadding - (WI.CPUTimelineOverviewGraph.samplingRatePerSecond / 2);
791                 this.selectionDuration = endTime - startTime + (selectionPadding * 2);
792             } else if (startTime < this.selectionStartTime || endTime > this.selectionStartTime + this.selectionDuration) {
793                 let selectionPadding = this.secondsPerPixel * 10;
794                 this.selectionStartTime = startTime - selectionPadding;
795                 this.selectionDuration = endTime - startTime + (selectionPadding * 2);
796             }
797         }
798
799         this.dispatchEventToListeners(WI.TimelineOverview.Event.RecordSelected, {record: this._selectedTimelineRecord});
800     }
801
802     _resetSelection()
803     {
804         function reset(settings)
805         {
806             settings.durationPerPixelSetting.reset();
807             settings.selectionStartValueSetting.reset();
808             settings.selectionDurationSetting.reset();
809         }
810
811         reset(this._timelinesViewModeSettings);
812         if (this._renderingFramesViewModeSettings)
813             reset(this._renderingFramesViewModeSettings);
814
815         this.secondsPerPixel = this._currentSettings.durationPerPixelSetting.value;
816         this.selectionStartTime = this._currentSettings.selectionStartValueSetting.value;
817         this.selectionDuration = this._currentSettings.selectionDurationSetting.value;
818     }
819
820     _recordingReset(event)
821     {
822         this._timelineRuler.clearMarkers();
823         this._timelineRuler.addMarker(this._currentTimeMarker);
824     }
825
826     _canShowTimelineType(type)
827     {
828         let timelineViewMode = WI.TimelineOverview.ViewMode.Timelines;
829         if (type === WI.TimelineRecord.Type.RenderingFrame)
830             timelineViewMode = WI.TimelineOverview.ViewMode.RenderingFrames;
831
832         return timelineViewMode === this._viewMode;
833     }
834
835     _viewModeDidChange()
836     {
837         let startTime = 0;
838         let isRenderingFramesMode = this._viewMode === WI.TimelineOverview.ViewMode.RenderingFrames;
839         if (isRenderingFramesMode) {
840             this._timelineRuler.minimumSelectionDuration = 1;
841             this._timelineRuler.snapInterval = 1;
842             this._timelineRuler.formatLabelCallback = (value) => value.maxDecimals(0).toLocaleString();
843         } else {
844             this._timelineRuler.minimumSelectionDuration = 0.01;
845             this._timelineRuler.snapInterval = NaN;
846             this._timelineRuler.formatLabelCallback = null;
847
848             startTime = this._startTime;
849         }
850
851         this.pixelAlignDuration = isRenderingFramesMode;
852         this.selectionStartTime = this._currentSettings.selectionStartValueSetting.value + startTime;
853         this.selectionDuration = this._currentSettings.selectionDurationSetting.value;
854
855         for (let [type, overviewGraph] of this._overviewGraphsByTypeMap) {
856             let treeElement = this._treeElementsByTypeMap.get(type);
857             console.assert(treeElement, "Missing tree element for timeline type", type);
858
859             treeElement.hidden = !this._canShowTimelineType(type);
860             if (treeElement.hidden)
861                 overviewGraph.hidden();
862             else
863                 overviewGraph.shown();
864         }
865
866         this.element.classList.toggle("frames", isRenderingFramesMode);
867
868         this.updateLayout(WI.View.LayoutReason.Resize);
869     }
870
871     _createViewModeSettings(viewMode, minimumDurationPerPixel, maximumDurationPerPixel, durationPerPixel, selectionStartValue, selectionDuration)
872     {
873         durationPerPixel = Math.min(maximumDurationPerPixel, Math.max(minimumDurationPerPixel, durationPerPixel));
874
875         let durationPerPixelSetting = new WI.Setting(viewMode + "-duration-per-pixel", durationPerPixel);
876         let selectionStartValueSetting = new WI.Setting(viewMode + "-selection-start-value", selectionStartValue);
877         let selectionDurationSetting = new WI.Setting(viewMode + "-selection-duration", selectionDuration);
878
879         return {
880             scrollStartTime: 0,
881             minimumDurationPerPixel,
882             maximumDurationPerPixel,
883             durationPerPixelSetting,
884             selectionStartValueSetting,
885             selectionDurationSetting
886         };
887     }
888
889     get _currentSettings()
890     {
891         return this._viewMode === WI.TimelineOverview.ViewMode.Timelines ? this._timelinesViewModeSettings : this._renderingFramesViewModeSettings;
892     }
893
894     _timelinesTreeSelectionDidChange(event)
895     {
896         let timeline = null;
897         let selectedTreeElement = this._timelinesTreeOutline.selectedTreeElement;
898         if (selectedTreeElement) {
899             timeline = selectedTreeElement.representedObject;
900             console.assert(timeline instanceof WI.Timeline, timeline);
901             console.assert(this._recording.timelines.get(timeline.type) === timeline, timeline);
902
903             for (let [type, overviewGraph] of this._overviewGraphsByTypeMap)
904                 overviewGraph.selected = type === timeline.type;
905         }
906
907         this._selectedTimeline = timeline;
908         this.dispatchEventToListeners(WI.TimelineOverview.Event.TimelineSelected);
909     }
910
911     _toggleEditingInstruments(event)
912     {
913         if (this._editingInstruments)
914             this._stopEditingInstruments();
915         else
916             this._startEditingInstruments();
917     }
918
919     _editingInstrumentsDidChange()
920     {
921         this.element.classList.toggle(WI.TimelineOverview.EditInstrumentsStyleClassName, this._editingInstruments);
922         this._timelineRuler.enabled = !this._editingInstruments;
923
924         this._updateWheelAndGestureHandlers();
925         this._updateEditInstrumentsButton();
926
927         this.dispatchEventToListeners(WI.TimelineOverview.Event.EditingInstrumentsDidChange);
928     }
929
930     _updateEditInstrumentsButton()
931     {
932         let newLabel = this._editingInstruments ? WI.UIString("Done") : WI.UIString("Edit");
933         this._editInstrumentsButton.label = newLabel;
934         this._editInstrumentsButton.activated = this._editingInstruments;
935         this._editInstrumentsButton.enabled = !WI.timelineManager.isCapturing();
936     }
937
938     _updateWheelAndGestureHandlers()
939     {
940         if (this._editingInstruments) {
941             this.element.removeEventListener("wheel", this._handleWheelEventListener);
942             this.element.removeEventListener("gesturestart", this._handleGestureStartEventListener);
943             this.element.removeEventListener("gesturechange", this._handleGestureChangeEventListener);
944             this.element.removeEventListener("gestureend", this._handleGestureEndEventListener);
945             this._handleWheelEventListener = null;
946             this._handleGestureStartEventListener = null;
947             this._handleGestureChangeEventListener = null;
948             this._handleGestureEndEventListener = null;
949         } else {
950             this._handleWheelEventListener = this._handleWheelEvent.bind(this);
951             this._handleGestureStartEventListener = this._handleGestureStart.bind(this);
952             this._handleGestureChangeEventListener = this._handleGestureChange.bind(this);
953             this._handleGestureEndEventListener = this._handleGestureEnd.bind(this);
954             this.element.addEventListener("wheel", this._handleWheelEventListener);
955             this.element.addEventListener("gesturestart", this._handleGestureStartEventListener);
956             this.element.addEventListener("gesturechange", this._handleGestureChangeEventListener);
957             this.element.addEventListener("gestureend", this._handleGestureEndEventListener);
958         }
959     }
960
961     _startEditingInstruments()
962     {
963         console.assert(this._viewMode === WI.TimelineOverview.ViewMode.Timelines);
964
965         if (this._editingInstruments)
966             return;
967
968         this._editingInstruments = true;
969
970         for (let type of this._instrumentTypes) {
971             let treeElement = this._treeElementsByTypeMap.get(type);
972             if (!treeElement) {
973                 let timeline = this._recording.timelines.get(type);
974                 console.assert(timeline, "Missing timeline for type " + type);
975
976                 const placeholder = true;
977                 treeElement = new WI.TimelineTreeElement(timeline, placeholder);
978
979                 let insertionIndex = insertionIndexForObjectInListSortedByFunction(treeElement, this._timelinesTreeOutline.children, this._compareTimelineTreeElements.bind(this));
980                 this._timelinesTreeOutline.insertChild(treeElement, insertionIndex);
981
982                 let placeholderGraph = new WI.View;
983                 placeholderGraph.element.classList.add("timeline-overview-graph");
984                 treeElement[WI.TimelineOverview.PlaceholderOverviewGraph] = placeholderGraph;
985                 this._graphsContainerView.insertSubviewBefore(placeholderGraph, this._graphsContainerView.subviews[insertionIndex]);
986             }
987
988             treeElement.editing = true;
989             treeElement.addEventListener(WI.TimelineTreeElement.Event.EnabledDidChange, this._timelineTreeElementEnabledDidChange, this);
990         }
991
992         this._editingInstrumentsDidChange();
993     }
994
995     _stopEditingInstruments()
996     {
997         if (!this._editingInstruments)
998             return;
999
1000         this._editingInstruments = false;
1001
1002         let instruments = this._recording.instruments;
1003         for (let treeElement of this._treeElementsByTypeMap.values()) {
1004             if (treeElement.status.checked) {
1005                 treeElement.editing = false;
1006                 treeElement.removeEventListener(WI.TimelineTreeElement.Event.EnabledDidChange, this._timelineTreeElementEnabledDidChange, this);
1007                 continue;
1008             }
1009
1010             let timelineInstrument = instruments.find((instrument) => instrument.timelineRecordType === treeElement.representedObject.type);
1011             this._recording.removeInstrument(timelineInstrument);
1012         }
1013
1014         let placeholderTreeElements = this._timelinesTreeOutline.children.filter((treeElement) => treeElement.placeholder);
1015         for (let treeElement of placeholderTreeElements) {
1016             this._timelinesTreeOutline.removeChild(treeElement);
1017
1018             let placeholderGraph = treeElement[WI.TimelineOverview.PlaceholderOverviewGraph];
1019             console.assert(placeholderGraph);
1020             this._graphsContainerView.removeSubview(placeholderGraph);
1021
1022             if (treeElement.status.checked) {
1023                 let instrument = WI.Instrument.createForTimelineType(treeElement.representedObject.type);
1024                 this._recording.addInstrument(instrument);
1025             }
1026         }
1027
1028         let instrumentTypes = instruments.map((instrument) => instrument.timelineRecordType);
1029         WI.timelineManager.enabledTimelineTypes = instrumentTypes;
1030
1031         this._editingInstrumentsDidChange();
1032     }
1033
1034     _handleTimelineCapturingStateChanged(event)
1035     {
1036         switch (WI.timelineManager.capturingState) {
1037         case WI.TimelineManager.CapturingState.Active:
1038             this._editInstrumentsButton.enabled = false;
1039             this._stopEditingInstruments();
1040             break;
1041
1042         case WI.TimelineManager.CapturingState.Inactive:
1043             this._editInstrumentsButton.enabled = true;
1044             break;
1045         }
1046     }
1047
1048     _recordingImported(event)
1049     {
1050         let {overviewData} = event.data;
1051
1052         if (overviewData.secondsPerPixel !== undefined)
1053             this.secondsPerPixel = overviewData.secondsPerPixel;
1054         if (overviewData.scrollStartTime !== undefined)
1055             this.scrollStartTime = overviewData.scrollStartTime;
1056         if (overviewData.selectionStartTime !== undefined)
1057             this.selectionStartTime = overviewData.selectionStartTime;
1058         if (overviewData.selectionDuration !== undefined) {
1059             if (overviewData.selectionDuration === Number.MAX_VALUE)
1060                 this._timelineRuler.selectEntireRange();
1061             else
1062                 this.selectionDuration = overviewData.selectionDuration;
1063         }
1064         if (overviewData.selectedTimelineType !== undefined) {
1065             let timeline = this._recording.timelineForRecordType(overviewData.selectedTimelineType);
1066             if (timeline)
1067                 this.selectedTimeline = timeline;
1068         }
1069     }
1070
1071     _compareTimelineTreeElements(a, b)
1072     {
1073         let aTimelineType = a.representedObject.type;
1074         let bTimelineType = b.representedObject.type;
1075
1076         // Always sort the Rendering Frames timeline last.
1077         if (aTimelineType === WI.TimelineRecord.Type.RenderingFrame)
1078             return 1;
1079         if (bTimelineType === WI.TimelineRecord.Type.RenderingFrame)
1080             return -1;
1081
1082         if (a.placeholder !== b.placeholder)
1083             return a.placeholder ? 1 : -1;
1084
1085         let aTimelineIndex = this._instrumentTypes.indexOf(aTimelineType);
1086         let bTimelineIndex = this._instrumentTypes.indexOf(bTimelineType);
1087         return aTimelineIndex - bTimelineIndex;
1088     }
1089
1090     _timelineTreeElementEnabledDidChange(event)
1091     {
1092         let enabled = this._timelinesTreeOutline.children.some((treeElement) => {
1093             let timelineType = treeElement.representedObject.type;
1094             return this._canShowTimelineType(timelineType) && treeElement.status.checked;
1095         });
1096
1097         this._editInstrumentsButton.enabled = enabled;
1098     }
1099 };
1100
1101 WI.TimelineOverview.PlaceholderOverviewGraph = Symbol("placeholder-overview-graph");
1102
1103 WI.TimelineOverview.ScrollDeltaDenominator = 500;
1104 WI.TimelineOverview.EditInstrumentsStyleClassName = "edit-instruments";
1105 WI.TimelineOverview.MinimumDurationPerPixel = 0.0001;
1106 WI.TimelineOverview.MaximumDurationPerPixel = 60;
1107
1108 WI.TimelineOverview.ViewMode = {
1109     Timelines: "timeline-overview-view-mode-timelines",
1110     RenderingFrames: "timeline-overview-view-mode-rendering-frames"
1111 };
1112
1113 WI.TimelineOverview.Event = {
1114     EditingInstrumentsDidChange: "editing-instruments-did-change",
1115     RecordSelected: "timeline-overview-record-selected",
1116     TimelineSelected: "timeline-overview-timeline-selected",
1117     TimeRangeSelectionChanged: "timeline-overview-time-range-selection-changed"
1118 };