Web Inspector: Timelines: can't reliably stop/start a recording
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / TimelineRecordingContentView.js
1 /*
2  * Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
3  * Copyright (C) 2015 University of Washington.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  * 1. Redistributions of source code must retain the above copyright
9  *    notice, this list of conditions and the following disclaimer.
10  * 2. Redistributions in binary form must reproduce the above copyright
11  *    notice, this list of conditions and the following disclaimer in the
12  *    documentation and/or other materials provided with the distribution.
13  *
14  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
15  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
16  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
17  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
18  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
24  * THE POSSIBILITY OF SUCH DAMAGE.
25  */
26
27 WI.TimelineRecordingContentView = class TimelineRecordingContentView extends WI.ContentView
28 {
29     constructor(recording)
30     {
31         super(recording);
32
33         this._recording = recording;
34
35         this.element.classList.add("timeline-recording");
36
37         this._timelineOverview = new WI.TimelineOverview(this._recording);
38         this._timelineOverview.addEventListener(WI.TimelineOverview.Event.TimeRangeSelectionChanged, this._timeRangeSelectionChanged, this);
39         this._timelineOverview.addEventListener(WI.TimelineOverview.Event.RecordSelected, this._recordSelected, this);
40         this._timelineOverview.addEventListener(WI.TimelineOverview.Event.TimelineSelected, this._timelineSelected, this);
41         this._timelineOverview.addEventListener(WI.TimelineOverview.Event.EditingInstrumentsDidChange, this._editingInstrumentsDidChange, this);
42         this.addSubview(this._timelineOverview);
43
44         const disableBackForward = true;
45         const disableFindBanner = true;
46         this._timelineContentBrowser = new WI.ContentBrowser(null, this, disableBackForward, disableFindBanner);
47         this._timelineContentBrowser.addEventListener(WI.ContentBrowser.Event.CurrentContentViewDidChange, this._currentContentViewDidChange, this);
48
49         this._entireRecordingPathComponent = this._createTimelineRangePathComponent(WI.UIString("Entire Recording"));
50         this._timelineSelectionPathComponent = this._createTimelineRangePathComponent();
51         this._timelineSelectionPathComponent.previousSibling = this._entireRecordingPathComponent;
52         this._selectedTimeRangePathComponent = this._entireRecordingPathComponent;
53
54         this._filterBarNavigationItem = new WI.FilterBarNavigationItem;
55         this._filterBarNavigationItem.filterBar.addEventListener(WI.FilterBar.Event.FilterDidChange, this._filterDidChange, this);
56         this._timelineContentBrowser.navigationBar.addNavigationItem(this._filterBarNavigationItem);
57         this.addSubview(this._timelineContentBrowser);
58
59         if (WI.sharedApp.debuggableType === WI.DebuggableType.Web) {
60             this._autoStopCheckboxNavigationItem = new WI.CheckboxNavigationItem("auto-stop-recording", WI.UIString("Stop recording once page loads"), WI.settings.timelinesAutoStop.value);
61             this._autoStopCheckboxNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
62             this._autoStopCheckboxNavigationItem.addEventListener(WI.CheckboxNavigationItem.Event.CheckedDidChange, this._handleAutoStopCheckboxCheckedDidChange, this);
63
64             WI.settings.timelinesAutoStop.addEventListener(WI.Setting.Event.Changed, this._handleTimelinesAutoStopSettingChanged, this);
65         }
66
67         this._exportButtonNavigationItem = new WI.ButtonNavigationItem("export", WI.UIString("Export"), "Images/Export.svg", 15, 15);
68         this._exportButtonNavigationItem.toolTip = WI.UIString("Export (%s)").format(WI.saveKeyboardShortcut.displayName);
69         this._exportButtonNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
70         this._exportButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
71         this._exportButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._exportButtonNavigationItemClicked, this);
72         this._exportButtonNavigationItem.enabled = false;
73
74         this._importButtonNavigationItem = new WI.ButtonNavigationItem("import", WI.UIString("Import"), "Images/Import.svg", 15, 15);
75         this._importButtonNavigationItem.toolTip = WI.UIString("Import");
76         this._importButtonNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
77         this._importButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
78         this._importButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._importButtonNavigationItemClicked, this);
79
80         this._clearTimelineNavigationItem = new WI.ButtonNavigationItem("clear-timeline", WI.UIString("Clear Timeline (%s)").format(WI.clearKeyboardShortcut.displayName), "Images/NavigationItemTrash.svg", 15, 15);
81         this._clearTimelineNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
82         this._clearTimelineNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._clearTimeline, this);
83
84         this._overviewTimelineView = new WI.OverviewTimelineView(recording);
85         this._overviewTimelineView.secondsPerPixel = this._timelineOverview.secondsPerPixel;
86
87         this._progressView = new WI.TimelineRecordingProgressView;
88         this._timelineContentBrowser.addSubview(this._progressView);
89
90         this._timelineViewMap = new Map;
91         this._pathComponentMap = new Map;
92
93         this._updating = false;
94         this._currentTime = NaN;
95         this._discontinuityStartTime = NaN;
96         this._lastUpdateTimestamp = NaN;
97         this._startTimeNeedsReset = true;
98         this._renderingFrameTimeline = null;
99
100         this._recording.addEventListener(WI.TimelineRecording.Event.InstrumentAdded, this._instrumentAdded, this);
101         this._recording.addEventListener(WI.TimelineRecording.Event.InstrumentRemoved, this._instrumentRemoved, this);
102         this._recording.addEventListener(WI.TimelineRecording.Event.Reset, this._recordingReset, this);
103         this._recording.addEventListener(WI.TimelineRecording.Event.Unloaded, this._recordingUnloaded, this);
104
105         WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStateChanged, this._handleTimelineCapturingStateChanged, this);
106
107         WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.Paused, this._debuggerPaused, this);
108         WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.Resumed, this._debuggerResumed, this);
109
110         WI.ContentView.addEventListener(WI.ContentView.Event.SelectionPathComponentsDidChange, this._contentViewSelectionPathComponentDidChange, this);
111         WI.ContentView.addEventListener(WI.ContentView.Event.SupplementalRepresentedObjectsDidChange, this._contentViewSupplementalRepresentedObjectsDidChange, this);
112
113         WI.TimelineView.addEventListener(WI.TimelineView.Event.RecordWasFiltered, this._handleTimelineViewRecordFiltered, this);
114         WI.TimelineView.addEventListener(WI.TimelineView.Event.RecordWasSelected, this._handleTimelineViewRecordSelected, this);
115         WI.TimelineView.addEventListener(WI.TimelineView.Event.ScannerShow, this._handleTimelineViewScannerShow, this);
116         WI.TimelineView.addEventListener(WI.TimelineView.Event.ScannerHide, this._handleTimelineViewScannerHide, this);
117
118         WI.notifications.addEventListener(WI.Notification.VisibilityStateDidChange, this._inspectorVisibilityStateChanged, this);
119
120         for (let instrument of this._recording.instruments)
121             this._instrumentAdded(instrument);
122
123         this.showOverviewTimelineView();
124
125         if (this._recording.imported) {
126             let {startTime, endTime} = this._recording;
127             this._updateTimes(startTime, endTime, endTime);
128         }
129     }
130
131     // Public
132
133     showOverviewTimelineView()
134     {
135         this._timelineContentBrowser.showContentView(this._overviewTimelineView);
136     }
137
138     showTimelineViewForTimeline(timeline)
139     {
140         console.assert(timeline instanceof WI.Timeline, timeline);
141         console.assert(this._timelineViewMap.has(timeline), timeline);
142         if (!this._timelineViewMap.has(timeline))
143             return;
144
145         let contentView = this._timelineContentBrowser.showContentView(this._timelineViewMap.get(timeline));
146
147         // FIXME: `WI.HeapAllocationsTimelineView` relies on it's `_dataGrid` for determining what
148         // object is currently selected. If that `_dataGrid` hasn't yet called `layout()` when first
149         // shown, we will lose the selection.
150         if (!contentView.didInitialLayout)
151             contentView.updateLayout();
152     }
153
154     get supportsSplitContentBrowser()
155     {
156         // The layout of the overview and split content browser don't work well.
157         return false;
158     }
159
160     get selectionPathComponents()
161     {
162         if (!this._timelineContentBrowser.currentContentView)
163             return [];
164
165         let pathComponents = [];
166         let representedObject = this._timelineContentBrowser.currentContentView.representedObject;
167         if (representedObject instanceof WI.Timeline)
168             pathComponents.push(this._pathComponentMap.get(representedObject));
169
170         pathComponents.push(this._selectedTimeRangePathComponent);
171         return pathComponents;
172     }
173
174     get supplementalRepresentedObjects()
175     {
176         if (!this._timelineContentBrowser.currentContentView)
177             return [];
178         return this._timelineContentBrowser.currentContentView.supplementalRepresentedObjects;
179     }
180
181     get navigationItems()
182     {
183         let navigationItems = [];
184         if (this._autoStopCheckboxNavigationItem)
185             navigationItems.push(this._autoStopCheckboxNavigationItem);
186         navigationItems.push(new WI.DividerNavigationItem);
187         navigationItems.push(this._importButtonNavigationItem);
188         navigationItems.push(this._exportButtonNavigationItem);
189         navigationItems.push(new WI.DividerNavigationItem);
190         navigationItems.push(this._clearTimelineNavigationItem);
191         return navigationItems;
192     }
193
194     get handleCopyEvent()
195     {
196         let currentContentView = this._timelineContentBrowser.currentContentView;
197         return currentContentView && typeof currentContentView.handleCopyEvent === "function" ? currentContentView.handleCopyEvent.bind(currentContentView) : null;
198     }
199
200     get supportsSave()
201     {
202         return this._recording.canExport();
203     }
204
205     get saveData()
206     {
207         return {customSaveHandler: () => { this._exportTimelineRecording(); }};
208     }
209
210     get currentTimelineView()
211     {
212         return this._timelineContentBrowser.currentContentView;
213     }
214
215     shown()
216     {
217         super.shown();
218
219         this._timelineOverview.shown();
220         this._timelineContentBrowser.shown();
221
222         this._clearTimelineNavigationItem.enabled = !this._recording.readonly && !isNaN(this._recording.startTime);
223         this._exportButtonNavigationItem.enabled = this._recording.canExport();
224
225         this._currentContentViewDidChange();
226
227         if (!this._updating && WI.timelineManager.activeRecording === this._recording && WI.timelineManager.isCapturing())
228             this._startUpdatingCurrentTime(this._currentTime);
229     }
230
231     hidden()
232     {
233         super.hidden();
234
235         this._timelineOverview.hidden();
236         this._timelineContentBrowser.hidden();
237
238         if (this._updating)
239             this._stopUpdatingCurrentTime();
240     }
241
242     closed()
243     {
244         super.closed();
245
246         this._timelineContentBrowser.contentViewContainer.closeAllContentViews();
247
248         this._recording.removeEventListener(null, null, this);
249
250         WI.timelineManager.removeEventListener(null, null, this);
251         WI.debuggerManager.removeEventListener(null, null, this);
252         WI.ContentView.removeEventListener(null, null, this);
253     }
254
255     canGoBack()
256     {
257         return this._timelineContentBrowser.canGoBack();
258     }
259
260     canGoForward()
261     {
262         return this._timelineContentBrowser.canGoForward();
263     }
264
265     goBack()
266     {
267         this._timelineContentBrowser.goBack();
268     }
269
270     goForward()
271     {
272         this._timelineContentBrowser.goForward();
273     }
274
275     handleClearShortcut(event)
276     {
277         this._clearTimeline();
278     }
279
280     // ContentBrowser delegate
281
282     contentBrowserTreeElementForRepresentedObject(contentBrowser, representedObject)
283     {
284         if (!(representedObject instanceof WI.Timeline) && !(representedObject instanceof WI.TimelineRecording))
285             return null;
286
287         let iconClassName;
288         let title;
289         if (representedObject instanceof WI.Timeline) {
290             iconClassName = WI.TimelineTabContentView.iconClassNameForTimelineType(representedObject.type);
291             title = WI.UIString("Details");
292         } else {
293             iconClassName = WI.TimelineTabContentView.StopwatchIconStyleClass;
294             title = WI.UIString("Overview");
295         }
296
297         const hasChildren = false;
298         return new WI.GeneralTreeElement(iconClassName, title, representedObject, hasChildren);
299     }
300
301     // Private
302
303     _currentContentViewDidChange(event)
304     {
305         let newViewMode;
306         let timelineView = this.currentTimelineView;
307         if (timelineView && timelineView.representedObject.type === WI.TimelineRecord.Type.RenderingFrame)
308             newViewMode = WI.TimelineOverview.ViewMode.RenderingFrames;
309         else
310             newViewMode = WI.TimelineOverview.ViewMode.Timelines;
311
312         this._timelineOverview.viewMode = newViewMode;
313         this._updateTimelineOverviewHeight();
314         this._updateProgressView();
315         this._updateFilterBar();
316
317         if (timelineView) {
318             this._updateTimelineViewTimes(timelineView);
319             this._filterDidChange();
320
321             let timeline = null;
322             if (timelineView.representedObject instanceof WI.Timeline)
323                 timeline = timelineView.representedObject;
324
325             this._timelineOverview.selectedTimeline = timeline;
326         }
327
328         this.dispatchEventToListeners(WI.ContentView.Event.SelectionPathComponentsDidChange);
329         this.dispatchEventToListeners(WI.ContentView.Event.NavigationItemsDidChange);
330     }
331
332     _timelinePathComponentSelected(event)
333     {
334         let selectedTimeline = event.data.pathComponent.representedObject;
335         this.showTimelineViewForTimeline(selectedTimeline);
336     }
337
338     _timeRangePathComponentSelected(event)
339     {
340         let selectedPathComponent = event.data.pathComponent;
341         if (selectedPathComponent === this._selectedTimeRangePathComponent)
342             return;
343
344         let timelineRuler = this._timelineOverview.timelineRuler;
345         if (selectedPathComponent === this._entireRecordingPathComponent)
346             timelineRuler.selectEntireRange();
347         else {
348             let timelineRange = selectedPathComponent.representedObject;
349             timelineRuler.selectionStartTime = timelineRuler.zeroTime + timelineRange.startValue;
350             timelineRuler.selectionEndTime = timelineRuler.zeroTime + timelineRange.endValue;
351         }
352     }
353
354     _contentViewSelectionPathComponentDidChange(event)
355     {
356         if (!this.visible)
357             return;
358
359         if (event.target !== this._timelineContentBrowser.currentContentView)
360             return;
361
362         this._updateFilterBar();
363
364         this.dispatchEventToListeners(WI.ContentView.Event.SelectionPathComponentsDidChange);
365
366         if (this.currentTimelineView === this._overviewTimelineView)
367             return;
368
369         let record = null;
370         if (this.currentTimelineView.selectionPathComponents) {
371             let recordPathComponent = this.currentTimelineView.selectionPathComponents.find((element) => element.representedObject instanceof WI.TimelineRecord);
372             record = recordPathComponent ? recordPathComponent.representedObject : null;
373         }
374
375         this._timelineOverview.selectRecord(event.target.representedObject, record);
376     }
377
378     _contentViewSupplementalRepresentedObjectsDidChange(event)
379     {
380         if (event.target !== this._timelineContentBrowser.currentContentView)
381             return;
382         this.dispatchEventToListeners(WI.ContentView.Event.SupplementalRepresentedObjectsDidChange);
383     }
384
385     _inspectorVisibilityStateChanged()
386     {
387         if (WI.timelineManager.activeRecording !== this._recording)
388             return;
389
390         // Stop updating since the results won't be rendered anyway.
391         if (!WI.visible && this._updating) {
392             this._stopUpdatingCurrentTime();
393             return;
394         }
395
396         // Nothing else to do if the current time was not being updated.
397         if (!WI.visible)
398             return;
399
400         let {startTime, endTime} = this.representedObject;
401         if (!WI.timelineManager.isCapturing()) {
402             // Force the overview to render data from the entire recording.
403             // This is necessary if the recording was started when the inspector was not
404             // visible because the views were never updated with currentTime/endTime.
405             this._updateTimes(startTime, endTime, endTime);
406             return;
407         }
408
409         this._startUpdatingCurrentTime(endTime);
410     }
411
412     _update(timestamp)
413     {
414         // FIXME: <https://webkit.org/b/153634> Web Inspector: some background tabs think they are the foreground tab and do unnecessary work
415         if (!(WI.tabBrowser.selectedTabContentView instanceof WI.TimelineTabContentView))
416             return;
417
418         if (this._waitingToResetCurrentTime) {
419             requestAnimationFrame(this._updateCallback);
420             return;
421         }
422
423         var startTime = this._recording.startTime;
424         var currentTime = this._currentTime || startTime;
425         var endTime = this._recording.endTime;
426         var timespanSinceLastUpdate = (timestamp - this._lastUpdateTimestamp) / 1000 || 0;
427
428         currentTime += timespanSinceLastUpdate;
429
430         this._updateTimes(startTime, currentTime, endTime);
431
432         // Only stop updating if the current time is greater than the end time, or the end time is NaN.
433         // The recording end time will be NaN if no records were added.
434         if (!this._updating && (currentTime >= endTime || isNaN(endTime))) {
435             if (this.visible)
436                 this._lastUpdateTimestamp = NaN;
437             return;
438         }
439
440         this._lastUpdateTimestamp = timestamp;
441
442         requestAnimationFrame(this._updateCallback);
443     }
444
445     _updateTimes(startTime, currentTime, endTime)
446     {
447         if (this._startTimeNeedsReset && !isNaN(startTime)) {
448             this._timelineOverview.startTime = startTime;
449             this._overviewTimelineView.zeroTime = startTime;
450             for (let timelineView of this._timelineViewMap.values())
451                 timelineView.zeroTime = startTime;
452
453             this._startTimeNeedsReset = false;
454         }
455
456         this._timelineOverview.endTime = Math.max(endTime, currentTime);
457
458         this._currentTime = currentTime;
459         this._timelineOverview.currentTime = currentTime;
460
461         if (this.currentTimelineView)
462             this._updateTimelineViewTimes(this.currentTimelineView);
463
464         // Force a layout now since we are already in an animation frame and don't need to delay it until the next.
465         this._timelineOverview.updateLayoutIfNeeded();
466         if (this.currentTimelineView)
467             this.currentTimelineView.updateLayoutIfNeeded();
468     }
469
470     _startUpdatingCurrentTime(startTime)
471     {
472         console.assert(!this._updating);
473         if (this._updating)
474             return;
475
476         // Don't update the current time if the Inspector is not visible, as the requestAnimationFrames won't work.
477         if (!WI.visible)
478             return;
479
480         if (typeof startTime === "number")
481             this._currentTime = startTime;
482         else if (!isNaN(this._currentTime)) {
483             // This happens when you stop and later restart recording.
484             // COMPATIBILITY (iOS 9): Timeline.recordingStarted events did not include a timestamp.
485             // We likely need to jump into the future to a better current time which we can
486             // ascertained from a new incoming timeline record, so we wait for a Timeline to update.
487             console.assert(!this._waitingToResetCurrentTime);
488             this._waitingToResetCurrentTime = true;
489             this._recording.addEventListener(WI.TimelineRecording.Event.TimesUpdated, this._recordingTimesUpdated, this);
490         }
491
492         this._updating = true;
493
494         if (!this._updateCallback)
495             this._updateCallback = this._update.bind(this);
496
497         requestAnimationFrame(this._updateCallback);
498     }
499
500     _stopUpdatingCurrentTime()
501     {
502         console.assert(this._updating);
503         this._updating = false;
504
505         if (this._waitingToResetCurrentTime) {
506             // Did not get any event while waiting for the current time, but we should stop waiting.
507             this._recording.removeEventListener(WI.TimelineRecording.Event.TimesUpdated, this._recordingTimesUpdated, this);
508             this._waitingToResetCurrentTime = false;
509         }
510     }
511
512     _handleTimelineCapturingStateChanged(event)
513     {
514         let {startTime, endTime} = event.data;
515
516         this._updateProgressView();
517
518         switch (WI.timelineManager.capturingState) {
519         case WI.TimelineManager.CapturingState.Active:
520             if (!this._updating)
521                 this._startUpdatingCurrentTime(startTime);
522
523             this._clearTimelineNavigationItem.enabled = !this._recording.readonly;
524             this._exportButtonNavigationItem.enabled = false;
525
526             // A discontinuity occurs when the recording is stopped and resumed at
527             // a future time. Capturing started signals the end of the current
528             // discontinuity, if one exists.
529             if (!isNaN(this._discontinuityStartTime)) {
530                 this._recording.addDiscontinuity(this._discontinuityStartTime, startTime);
531                 this._discontinuityStartTime = NaN;
532             }
533             break;
534
535         case WI.TimelineManager.CapturingState.Inactive:
536             if (this._updating)
537                 this._stopUpdatingCurrentTime();
538
539             if (this.currentTimelineView)
540                 this._updateTimelineViewTimes(this.currentTimelineView);
541
542             this._discontinuityStartTime = endTime || this._currentTime;
543
544             this._exportButtonNavigationItem.enabled = this._recording.canExport();
545             break;
546         }
547     }
548
549     _debuggerPaused(event)
550     {
551         if (this._updating)
552             this._stopUpdatingCurrentTime();
553     }
554
555     _debuggerResumed(event)
556     {
557         if (!this._updating)
558             this._startUpdatingCurrentTime();
559     }
560
561     _recordingTimesUpdated(event)
562     {
563         if (!this._waitingToResetCurrentTime)
564             return;
565
566         // COMPATIBILITY (iOS 9): Timeline.recordingStarted events did not include a new startTime.
567         // Make the current time be the start time of the last added record. This is the best way
568         // currently to jump to the right period of time after recording starts.
569
570         for (var timeline of this._recording.timelines.values()) {
571             var lastRecord = timeline.records.lastValue;
572             if (!lastRecord)
573                 continue;
574             this._currentTime = Math.max(this._currentTime, lastRecord.startTime);
575         }
576
577         this._recording.removeEventListener(WI.TimelineRecording.Event.TimesUpdated, this._recordingTimesUpdated, this);
578         this._waitingToResetCurrentTime = false;
579     }
580
581     _handleAutoStopCheckboxCheckedDidChange(event)
582     {
583         WI.settings.timelinesAutoStop.value = this._autoStopCheckboxNavigationItem.checked;
584     }
585
586     _handleTimelinesAutoStopSettingChanged(event)
587     {
588         this._autoStopCheckboxNavigationItem.checked = WI.settings.timelinesAutoStop.value;
589     }
590
591     _exportTimelineRecording()
592     {
593         let json = {
594             version: WI.TimelineRecording.SerializationVersion,
595             recording: this._recording.exportData(),
596             overview: this._timelineOverview.exportData(),
597         };
598         if (!json.recording || !json.overview) {
599             InspectorFrontendHost.beep();
600             return;
601         }
602
603         let frameName = null;
604         let mainFrame = WI.networkManager.mainFrame;
605         if (mainFrame)
606             frameName = mainFrame.mainResource.urlComponents.host || mainFrame.mainResource.displayName;
607
608         let filename = frameName ? `${frameName}-recording` : this._recording.displayName;
609
610         WI.FileUtilities.save({
611             url: WI.FileUtilities.inspectorURLForFilename(filename + ".json"),
612             content: JSON.stringify(json),
613             forceSaveAs: true,
614         });
615     }
616
617     _exportButtonNavigationItemClicked(event)
618     {
619         this._exportTimelineRecording();
620     }
621
622     _importButtonNavigationItemClicked(event)
623     {
624         WI.FileUtilities.importJSON((result) => WI.timelineManager.processJSON(result));
625     }
626
627     _clearTimeline(event)
628     {
629         if (this._recording.readonly)
630             return;
631
632         if (WI.timelineManager.activeRecording === this._recording && WI.timelineManager.isCapturing())
633             WI.timelineManager.stopCapturing();
634
635         this._recording.reset();
636     }
637
638     _updateTimelineOverviewHeight()
639     {
640         if (this._timelineOverview.editingInstruments)
641             this._timelineOverview.element.style.height = "";
642         else {
643             const rulerHeight = 23;
644
645             let styleValue = (rulerHeight + this._timelineOverview.height) + "px";
646             this._timelineOverview.element.style.height = styleValue;
647             this._timelineContentBrowser.element.style.top = styleValue;
648         }
649     }
650
651     _instrumentAdded(instrumentOrEvent)
652     {
653         let instrument = instrumentOrEvent instanceof WI.Instrument ? instrumentOrEvent : instrumentOrEvent.data.instrument;
654         console.assert(instrument instanceof WI.Instrument, instrument);
655
656         let timeline = this._recording.timelineForInstrument(instrument);
657         console.assert(!this._timelineViewMap.has(timeline), timeline);
658
659         this._timelineViewMap.set(timeline, WI.ContentView.createFromRepresentedObject(timeline, {recording: this._recording}));
660         if (timeline.type === WI.TimelineRecord.Type.RenderingFrame)
661             this._renderingFrameTimeline = timeline;
662
663         let displayName = WI.TimelineTabContentView.displayNameForTimelineType(timeline.type);
664         let iconClassName = WI.TimelineTabContentView.iconClassNameForTimelineType(timeline.type);
665         let pathComponent = new WI.HierarchicalPathComponent(displayName, iconClassName, timeline);
666         pathComponent.addEventListener(WI.HierarchicalPathComponent.Event.SiblingWasSelected, this._timelinePathComponentSelected, this);
667         this._pathComponentMap.set(timeline, pathComponent);
668
669         this._timelineCountChanged();
670     }
671
672     _instrumentRemoved(event)
673     {
674         let instrument = event.data.instrument;
675         console.assert(instrument instanceof WI.Instrument);
676
677         let timeline = this._recording.timelineForInstrument(instrument);
678         console.assert(this._timelineViewMap.has(timeline), timeline);
679
680         let timelineView = this._timelineViewMap.take(timeline);
681         if (this.currentTimelineView === timelineView)
682             this.showOverviewTimelineView();
683         if (timeline.type === WI.TimelineRecord.Type.RenderingFrame)
684             this._renderingFrameTimeline = null;
685
686         this._pathComponentMap.delete(timeline);
687
688         this._timelineCountChanged();
689     }
690
691     _timelineCountChanged()
692     {
693         var previousPathComponent = null;
694         for (var pathComponent of this._pathComponentMap.values()) {
695             if (previousPathComponent) {
696                 previousPathComponent.nextSibling = pathComponent;
697                 pathComponent.previousSibling = previousPathComponent;
698             }
699
700             previousPathComponent = pathComponent;
701         }
702
703         this._updateTimelineOverviewHeight();
704     }
705
706     _recordingReset(event)
707     {
708         for (let timelineView of this._timelineViewMap.values())
709             timelineView.reset();
710
711         this._currentTime = NaN;
712         this._discontinuityStartTime = NaN;
713
714         if (!this._updating) {
715             // Force the time ruler and views to reset to 0.
716             this._startTimeNeedsReset = true;
717             this._updateTimes(0, 0, 0);
718         }
719
720         this._lastUpdateTimestamp = NaN;
721         this._startTimeNeedsReset = true;
722
723         this._recording.removeEventListener(WI.TimelineRecording.Event.TimesUpdated, this._recordingTimesUpdated, this);
724         this._waitingToResetCurrentTime = false;
725
726         this._timelineOverview.reset();
727         this._overviewTimelineView.reset();
728         this._clearTimelineNavigationItem.enabled = false;
729         this._exportButtonNavigationItem.enabled = false;
730     }
731
732     _recordingUnloaded(event)
733     {
734         console.assert(!this._updating);
735
736         WI.timelineManager.removeEventListener(null, null, this);
737     }
738
739     _timeRangeSelectionChanged(event)
740     {
741         console.assert(this.currentTimelineView);
742         if (!this.currentTimelineView)
743             return;
744
745         this._updateTimelineViewTimes(this.currentTimelineView);
746
747         let selectedPathComponent;
748         if (this._timelineOverview.timelineRuler.entireRangeSelected)
749             selectedPathComponent = this._entireRecordingPathComponent;
750         else {
751             let timelineRange = this._timelineSelectionPathComponent.representedObject;
752             timelineRange.startValue = this.currentTimelineView.startTime;
753             timelineRange.endValue = this.currentTimelineView.endTime;
754
755             if (!(this.currentTimelineView instanceof WI.RenderingFrameTimelineView)) {
756                 timelineRange.startValue -= this.currentTimelineView.zeroTime;
757                 timelineRange.endValue -= this.currentTimelineView.zeroTime;
758             }
759
760             this._updateTimeRangePathComponents();
761             selectedPathComponent = this._timelineSelectionPathComponent;
762         }
763
764         if (this._selectedTimeRangePathComponent !== selectedPathComponent) {
765             this._selectedTimeRangePathComponent = selectedPathComponent;
766             this.dispatchEventToListeners(WI.ContentView.Event.SelectionPathComponentsDidChange);
767         }
768     }
769
770     _recordSelected(event)
771     {
772         let {record} = event.data;
773
774         this._selectRecordInTimelineView(record);
775     }
776
777     _timelineSelected()
778     {
779         let timeline = this._timelineOverview.selectedTimeline;
780         if (timeline)
781             this.showTimelineViewForTimeline(timeline);
782         else
783             this.showOverviewTimelineView();
784     }
785
786     _updateTimeRangePathComponents()
787     {
788         let timelineRange = this._timelineSelectionPathComponent.representedObject;
789         let startValue = timelineRange.startValue;
790         let endValue = timelineRange.endValue;
791         if (isNaN(startValue) || isNaN(endValue)) {
792             this._entireRecordingPathComponent.nextSibling = null;
793             return;
794         }
795
796         this._entireRecordingPathComponent.nextSibling = this._timelineSelectionPathComponent;
797
798         let displayName;
799         if (this._timelineOverview.viewMode === WI.TimelineOverview.ViewMode.Timelines) {
800             const higherResolution = true;
801             let selectionStart = Number.secondsToString(startValue, higherResolution);
802             let selectionEnd = Number.secondsToString(endValue, higherResolution);
803             const epsilon = 0.0001;
804             if (startValue < epsilon)
805                 displayName = WI.UIString("%s \u2013 %s").format(selectionStart, selectionEnd);
806             else {
807                 let duration = Number.secondsToString(endValue - startValue, higherResolution);
808                 displayName = WI.UIString("%s \u2013 %s (%s)").format(selectionStart, selectionEnd, duration);
809             }
810         } else {
811             startValue += 1; // Convert index to frame number.
812             if (startValue === endValue)
813                 displayName = WI.UIString("Frame %d").format(startValue);
814             else
815                 displayName = WI.UIString("Frames %d \u2013 %d").format(startValue, endValue);
816         }
817
818         this._timelineSelectionPathComponent.displayName = displayName;
819         this._timelineSelectionPathComponent.title = displayName;
820     }
821
822     _createTimelineRangePathComponent(title)
823     {
824         let range = new WI.TimelineRange(NaN, NaN);
825         let pathComponent = new WI.HierarchicalPathComponent(title || enDash, "time-icon", range);
826         pathComponent.addEventListener(WI.HierarchicalPathComponent.Event.SiblingWasSelected, this._timeRangePathComponentSelected, this);
827
828         return pathComponent;
829     }
830
831     _updateTimelineViewTimes(timelineView)
832     {
833         let timelineRuler = this._timelineOverview.timelineRuler;
834         let entireRangeSelected = timelineRuler.entireRangeSelected;
835         let endTime = this._timelineOverview.selectionStartTime + this._timelineOverview.selectionDuration;
836
837         if (entireRangeSelected) {
838             if (timelineView instanceof WI.RenderingFrameTimelineView) {
839                 endTime = this._renderingFrameTimeline.records.length;
840             } else {
841                 // Clamp selection to the end of the recording (with padding),
842                 // so graph views will show an auto-sized graph without a lot of
843                 // empty space at the end.
844                 endTime = isNaN(this._recording.endTime) ? this._recording.currentTime : this._recording.endTime;
845                 endTime += timelineRuler.minimumSelectionDuration;
846             }
847         }
848
849         timelineView.startTime = this._timelineOverview.selectionStartTime;
850         timelineView.currentTime = this._currentTime;
851         timelineView.endTime = endTime;
852     }
853
854     _editingInstrumentsDidChange(event)
855     {
856         let editingInstruments = this._timelineOverview.editingInstruments;
857         this.element.classList.toggle(WI.TimelineOverview.EditInstrumentsStyleClassName, editingInstruments);
858
859         this._updateTimelineOverviewHeight();
860     }
861
862     _filterDidChange()
863     {
864         if (!this.currentTimelineView)
865             return;
866
867         this.currentTimelineView.updateFilter(this._filterBarNavigationItem.filterBar.filters);
868     }
869
870     _handleTimelineViewRecordFiltered(event)
871     {
872         if (event.target !== this.currentTimelineView)
873             return;
874
875         console.assert(this.currentTimelineView);
876
877         let timeline = this.currentTimelineView.representedObject;
878         if (!(timeline instanceof WI.Timeline))
879             return;
880
881         let record = event.data.record;
882         let filtered = event.data.filtered;
883         this._timelineOverview.recordWasFiltered(timeline, record, filtered);
884     }
885
886     _handleTimelineViewRecordSelected(event)
887     {
888         if (!this.visible)
889             return;
890
891         let {record} = event.data;
892
893         this._selectRecordInTimelineOverview(record);
894         this._selectRecordInTimelineView(record);
895     }
896
897     _selectRecordInTimelineOverview(record)
898     {
899         let timeline = this._recording.timelineForRecordType(record.type);
900         if (!timeline)
901             return;
902
903         this._timelineOverview.selectRecord(timeline, record);
904     }
905
906     _selectRecordInTimelineView(record)
907     {
908         for (let timelineView of this._timelineViewMap.values()) {
909             let recordMatchesTimeline = record && timelineView.representedObject.type === record.type;
910
911             if (recordMatchesTimeline && timelineView !== this.currentTimelineView)
912                 this.showTimelineViewForTimeline(timelineView.representedObject);
913
914             if (!record || recordMatchesTimeline)
915                 timelineView.selectRecord(record);
916         }
917     }
918
919     _handleTimelineViewScannerShow(event)
920     {
921         if (!this.visible)
922             return;
923
924         let {time} = event.data;
925         this._timelineOverview.showScanner(time);
926     }
927
928     _handleTimelineViewScannerHide(event)
929     {
930         if (!this.visible)
931             return;
932
933         this._timelineOverview.hideScanner();
934     }
935
936     _updateProgressView()
937     {
938         let isCapturing = WI.timelineManager.isCapturing();
939         this._progressView.visible = isCapturing && this.currentTimelineView && !this.currentTimelineView.showsLiveRecordingData;
940     }
941
942     _updateFilterBar()
943     {
944         this._filterBarNavigationItem.hidden = !this.currentTimelineView || !this.currentTimelineView.showsFilterBar;
945     }
946 };