Web Inspector: TimelineViews should be displayed in a ContentViewContainer
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / TimelineRecordingContentView.js
1 /*
2  * Copyright (C) 2013 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 WebInspector.TimelineRecordingContentView = function(recording)
28 {
29     WebInspector.ContentView.call(this, recording);
30
31     this._recording = recording;
32
33     this.element.classList.add(WebInspector.TimelineRecordingContentView.StyleClassName);
34
35     this._timelineOverview = new WebInspector.TimelineOverview(this._recording);
36     this._timelineOverview.addEventListener(WebInspector.TimelineOverview.Event.TimeRangeSelectionChanged, this._timeRangeSelectionChanged, this);
37     this.element.appendChild(this._timelineOverview.element);
38
39     this._contentViewContainer = new WebInspector.ContentViewContainer();
40     this._contentViewContainer.addEventListener(WebInspector.ContentViewContainer.Event.CurrentContentViewDidChange, this._currentContentViewDidChange, this);
41     this.element.appendChild(this._contentViewContainer.element);
42
43     var trashImage;
44     if (WebInspector.Platform.isLegacyMacOS)
45         trashImage = {src: "Images/Legacy/NavigationItemTrash.svg", width: 16, height: 16};
46     else
47         trashImage = {src: "Images/NavigationItemTrash.svg", width: 15, height: 15};
48
49     this._clearTimelineNavigationItem = new WebInspector.ButtonNavigationItem("clear-timeline", WebInspector.UIString("Clear Timeline"), trashImage.src, trashImage.width, trashImage.height);
50     this._clearTimelineNavigationItem.addEventListener(WebInspector.ButtonNavigationItem.Event.Clicked, this._clearTimeline, this);
51
52     this._overviewTimelineView = new WebInspector.OverviewTimelineView(recording);
53     this._overviewTimelineView.secondsPerPixel = this._timelineOverview.secondsPerPixel;
54
55     this._timelineViewMap = new Map;
56     this._pathComponentMap = new Map;
57
58     this._updating = false;
59     this._currentTime = NaN;
60     this._lastUpdateTimestamp = NaN;
61     this._startTimeNeedsReset = true;
62
63     this._recording.addEventListener(WebInspector.TimelineRecording.Event.TimelineAdded, this._timelineAdded, this);
64     this._recording.addEventListener(WebInspector.TimelineRecording.Event.TimelineRemoved, this._timelineRemoved, this);
65     this._recording.addEventListener(WebInspector.TimelineRecording.Event.Reset, this._recordingReset, this);
66     this._recording.addEventListener(WebInspector.TimelineRecording.Event.Unloaded, this._recordingUnloaded, this);
67
68     WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.Event.CapturingStarted, this._capturingStarted, this);
69     WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.Event.CapturingStopped, this._capturingStopped, this);
70
71     WebInspector.debuggerManager.addEventListener(WebInspector.DebuggerManager.Event.Paused, this._debuggerPaused, this);
72     WebInspector.debuggerManager.addEventListener(WebInspector.DebuggerManager.Event.Resumed, this._debuggerResumed, this);
73
74     WebInspector.ContentView.addEventListener(WebInspector.ContentView.Event.SelectionPathComponentsDidChange, this._contentViewSelectionPathComponentDidChange, this);
75     WebInspector.ContentView.addEventListener(WebInspector.ContentView.Event.SupplementalRepresentedObjectsDidChange, this._contentViewSupplementalRepresentedObjectsDidChange, this);
76
77     for (var timeline of this._recording.timelines.values())
78         this._timelineAdded(timeline);
79
80     this.showOverviewTimelineView();
81 };
82
83 WebInspector.TimelineRecordingContentView.StyleClassName = "timeline-recording";
84
85 WebInspector.TimelineRecordingContentView.SelectedTimelineTypeCookieKey = "timeline-recording-content-view-selected-timeline-type";
86 WebInspector.TimelineRecordingContentView.OverviewTimelineViewCookieValue = "timeline-recording-content-view-overview-timeline-view";
87
88 WebInspector.TimelineRecordingContentView.prototype = {
89     constructor: WebInspector.TimelineRecordingContentView,
90     __proto__: WebInspector.ContentView.prototype,
91
92     // Public
93
94     showOverviewTimelineView: function()
95     {
96         this._contentViewContainer.showContentView(this._overviewTimelineView);
97     },
98
99     showTimelineViewForTimeline: function(timeline)
100     {
101         console.assert(timeline instanceof WebInspector.Timeline, timeline);
102         console.assert(this._timelineViewMap.has(timeline), timeline);
103         if (!this._timelineViewMap.has(timeline))
104             return;
105
106         this._contentViewContainer.showContentView(this._timelineViewMap.get(timeline));
107     },
108
109     get allowedNavigationSidebarPanels()
110     {
111         return [WebInspector.timelineSidebarPanel.identifier];
112     },
113
114     get supportsSplitContentBrowser()
115     {
116         // The layout of the overview and split content browser don't work well.
117         return false;
118     },
119
120     get selectionPathComponents()
121     {
122         if (!this._contentViewContainer.currentContentView)
123             return [];
124
125         var pathComponents = this._contentViewContainer.currentContentView.selectionPathComponents || [];
126         var representedObject = this._contentViewContainer.currentContentView.representedObject;
127         if (representedObject instanceof WebInspector.Timeline)
128             pathComponents.unshift(this._pathComponentMap.get(representedObject));
129         return pathComponents;
130     },
131
132     get supplementalRepresentedObjects()
133     {
134         if (!this._contentViewContainer.currentContentView)
135             return [];
136         return this._contentViewContainer.currentContentView.supplementalRepresentedObjects;
137     },
138
139     get navigationItems()
140     {
141         return [this._clearTimelineNavigationItem];
142     },
143
144     get handleCopyEvent()
145     {
146         var currentContentView = this._contentViewContainer.currentContentView;
147         return currentContentView && typeof currentContentView.handleCopyEvent === "function" ? currentContentView.handleCopyEvent.bind(currentContentView) : null;
148     },
149
150     get supportsSave()
151     {
152         var currentContentView = this._contentViewContainer.currentContentView;
153         return currentContentView && currentContentView.supportsSave;
154     },
155
156     get saveData()
157     {
158         var currentContentView = this._contentViewContainer.currentContentView;
159         return currentContentView && currentContentView.saveData || null;
160     },
161
162     get currentTimelineView()
163     {
164         var contentView = this._contentViewContainer.currentContentView;
165         return (contentView instanceof WebInspector.TimelineView) ? contentView : null;
166     },
167
168     shown: function()
169     {
170         this._timelineOverview.shown();
171         this._contentViewContainer.shown();
172         this._clearTimelineNavigationItem.enabled = this._recording.isWritable();
173
174         if (!this._updating && WebInspector.timelineManager.activeRecording === this._recording && WebInspector.timelineManager.isCapturing())
175             this._startUpdatingCurrentTime();
176     },
177
178     hidden: function()
179     {
180         this._timelineOverview.hidden();
181         this._contentViewContainer.hidden();
182
183         if (this._updating)
184             this._stopUpdatingCurrentTime();
185     },
186
187     closed: function()
188     {
189         this._contentViewContainer.closeAllContentViews();
190
191         WebInspector.ContentView.removeEventListener(WebInspector.ContentView.Event.SelectionPathComponentsDidChange, this._contentViewSelectionPathComponentDidChange, this);
192         WebInspector.ContentView.removeEventListener(WebInspector.ContentView.Event.SupplementalRepresentedObjectsDidChange, this._contentViewSupplementalRepresentedObjectsDidChange, this);
193     },
194
195     canGoBack: function()
196     {
197         return this._contentViewContainer.canGoBack();
198     },
199
200     canGoForward: function()
201     {
202         return this._contentViewContainer.canGoForward();
203     },
204
205     goBack: function()
206     {
207         this._contentViewContainer.goBack();
208     },
209
210     goForward: function()
211     {
212         this._contentViewContainer.goForward();
213     },
214
215     updateLayout: function()
216     {
217         this._timelineOverview.updateLayoutForResize();
218
219         var currentContentView = this._contentViewContainer.currentContentView;
220         if (currentContentView)
221             currentContentView.updateLayout();
222     },
223
224     saveToCookie: function(cookie)
225     {
226         cookie.type = WebInspector.ContentViewCookieType.Timelines;
227
228         var currentContentView = this._contentViewContainer.currentContentView;
229         if (!currentContentView || currentContentView === this._overviewTimelineView)
230             cookie[WebInspector.TimelineRecordingContentView.SelectedTimelineTypeCookieKey] = WebInspector.TimelineRecordingContentView.OverviewTimelineViewCookieValue;
231         else if (currentContentView.representedObject instanceof WebInspector.Timeline)
232             cookie[WebInspector.TimelineRecordingContentView.SelectedTimelineTypeCookieKey] = this.currentTimelineView.representedObject.type;
233     },
234
235     restoreFromCookie: function(cookie)
236     {
237         var timelineType = cookie[WebInspector.TimelineRecordingContentView.SelectedTimelineTypeCookieKey];
238
239         if (timelineType === WebInspector.TimelineRecordingContentView.OverviewTimelineViewCookieValue)
240             this.showOverviewTimelineView();
241         else
242             this.showTimelineViewForTimeline(this.representedObject.timelines.get(timelineType));
243     },
244
245     filterDidChange: function()
246     {
247         if (!this.currentTimelineView)
248             return;
249
250         this.currentTimelineView.filterDidChange();
251     },
252
253     matchTreeElementAgainstCustomFilters: function(treeElement)
254     {
255         if (this.currentTimelineView && !this.currentTimelineView.matchTreeElementAgainstCustomFilters(treeElement))
256             return false;
257
258         var startTime = this._timelineOverview.selectionStartTime;
259         var endTime = this._timelineOverview.selectionStartTime + this._timelineOverview.selectionDuration;
260         var currentTime = this._currentTime || this._recording.startTime;
261
262         function checkTimeBounds(itemStartTime, itemEndTime)
263         {
264             itemStartTime = itemStartTime || currentTime;
265             itemEndTime = itemEndTime || currentTime;
266
267             return startTime <= itemEndTime && itemStartTime <= endTime;
268         }
269
270         if (treeElement instanceof WebInspector.ResourceTreeElement) {
271             var resource = treeElement.resource;
272             return checkTimeBounds(resource.requestSentTimestamp, resource.finishedOrFailedTimestamp);
273         }
274
275         if (treeElement instanceof WebInspector.SourceCodeTimelineTreeElement) {
276             var sourceCodeTimeline = treeElement.sourceCodeTimeline;
277
278             // Do a quick check of the timeline bounds before we check each record.
279             if (!checkTimeBounds(sourceCodeTimeline.startTime, sourceCodeTimeline.endTime))
280                 return false;
281
282             for (var record of sourceCodeTimeline.records) {
283                 if (checkTimeBounds(record.startTime, record.endTime))
284                     return true;
285             }
286
287             return false;
288         }
289
290         if (treeElement instanceof WebInspector.ProfileNodeTreeElement) {
291             var profileNode = treeElement.profileNode;
292             for (var call of profileNode.calls) {
293                 if (checkTimeBounds(call.startTime, call.endTime))
294                     return true;
295             }
296
297             return false;
298         }
299
300         if (treeElement instanceof WebInspector.TimelineRecordTreeElement) {
301             var record = treeElement.record;
302             return checkTimeBounds(record.startTime, record.endTime);
303         }
304
305         console.error("Unknown TreeElement, can't filter by time.");
306         return true;
307     },
308
309     // Private
310
311     _currentContentViewDidChange: function(event)
312     {
313         var timelineView = this.currentTimelineView;
314         if (timelineView) {
315             WebInspector.timelineSidebarPanel.contentTreeOutline = timelineView.navigationSidebarTreeOutline;
316             WebInspector.timelineSidebarPanel.contentTreeOutlineLabel = timelineView.navigationSidebarTreeOutlineLabel;
317
318             timelineView.startTime = this._timelineOverview.selectionStartTime;
319             timelineView.endTime = this._timelineOverview.selectionStartTime + this._timelineOverview.selectionDuration;
320             timelineView.currentTime = this._currentTime;
321         }
322
323         this.dispatchEventToListeners(WebInspector.ContentView.Event.SelectionPathComponentsDidChange);
324         this.dispatchEventToListeners(WebInspector.ContentView.Event.NavigationItemsDidChange);
325     },
326
327     _pathComponentSelected: function(event)
328     {
329         WebInspector.timelineSidebarPanel.showTimelineViewForTimeline(event.data.pathComponent.representedObject);
330     },
331
332     _contentViewSelectionPathComponentDidChange: function(event)
333     {
334         if (event.target !== this._contentViewContainer.currentContentView)
335             return;
336         this.dispatchEventToListeners(WebInspector.ContentView.Event.SelectionPathComponentsDidChange);
337     },
338
339     _contentViewSupplementalRepresentedObjectsDidChange: function(event)
340     {
341         if (event.target !== this._contentViewContainer.currentContentView)
342             return;
343         this.dispatchEventToListeners(WebInspector.ContentView.Event.SupplementalRepresentedObjectsDidChange);
344     },
345
346     _update: function(timestamp)
347     {
348         if (this._waitingToResetCurrentTime) {
349             requestAnimationFrame(this._updateCallback);
350             return;
351         }
352
353         var startTime = this._recording.startTime;
354         var currentTime = this._currentTime || startTime;
355         var endTime = this._recording.endTime;
356         var timespanSinceLastUpdate = (timestamp - this._lastUpdateTimestamp) / 1000 || 0;
357
358         currentTime += timespanSinceLastUpdate;
359
360         this._updateTimes(startTime, currentTime, endTime);
361
362         // Only stop updating if the current time is greater than the end time.
363         if (!this._updating && currentTime >= endTime) {
364             this._lastUpdateTimestamp = NaN;
365             return;
366         }
367
368         this._lastUpdateTimestamp = timestamp;
369
370         requestAnimationFrame(this._updateCallback);
371     },
372
373     _updateTimes: function(startTime, currentTime, endTime)
374     {
375         if (this._startTimeNeedsReset && !isNaN(startTime)) {
376             var selectionOffset = this._timelineOverview.selectionStartTime - this._timelineOverview.startTime;
377
378             this._timelineOverview.startTime = startTime;
379             this._timelineOverview.selectionStartTime = startTime + selectionOffset;
380
381             this._overviewTimelineView.zeroTime = startTime;
382             for (var timelineView of this._timelineViewMap.values())
383                 timelineView.zeroTime = startTime;
384
385             delete this._startTimeNeedsReset;
386         }
387
388         this._timelineOverview.endTime = Math.max(endTime, currentTime);
389
390         this._currentTime = currentTime;
391         this._timelineOverview.currentTime = currentTime;
392         if (this.currentTimelineView)
393             this.currentTimelineView.currentTime = currentTime;
394
395         WebInspector.timelineSidebarPanel.updateFilter();
396
397         // Force a layout now since we are already in an animation frame and don't need to delay it until the next.
398         this._timelineOverview.updateLayoutIfNeeded();
399         if (this.currentTimelineView)
400             this.currentTimelineView.updateLayoutIfNeeded();
401     },
402
403     _startUpdatingCurrentTime: function()
404     {
405         console.assert(!this._updating);
406         if (this._updating)
407             return;
408
409         if (!isNaN(this._currentTime)) {
410             // We have a current time already, so we likely need to jump into the future to a better current time.
411             // This happens when you stop and later restart recording.
412             console.assert(!this._waitingToResetCurrentTime);
413             this._waitingToResetCurrentTime = true;
414             this._recording.addEventListener(WebInspector.TimelineRecording.Event.TimesUpdated, this._recordingTimesUpdated, this);
415         }
416
417         this._updating = true;
418
419         if (!this._updateCallback)
420             this._updateCallback = this._update.bind(this);
421
422         requestAnimationFrame(this._updateCallback);
423     },
424
425     _stopUpdatingCurrentTime: function()
426     {
427         console.assert(this._updating);
428         this._updating = false;
429
430         if (this._waitingToResetCurrentTime) {
431             // Did not get any event while waiting for the current time, but we should stop waiting.
432             this._recording.removeEventListener(WebInspector.TimelineRecording.Event.TimesUpdated, this._recordingTimesUpdated, this);
433             this._waitingToResetCurrentTime = false;
434         }
435     },
436
437     _capturingStarted: function(event)
438     {
439         this._startUpdatingCurrentTime();
440     },
441
442     _capturingStopped: function(event)
443     {
444         if (this._updating)
445             this._stopUpdatingCurrentTime();
446     },
447
448     _debuggerPaused: function(event)
449     {
450         if (WebInspector.replayManager.sessionState === WebInspector.ReplayManager.SessionState.Replaying)
451             return;
452
453         this._stopUpdatingCurrentTime();
454     },
455
456     _debuggerResumed: function(event)
457     {
458         if (WebInspector.replayManager.sessionState === WebInspector.ReplayManager.SessionState.Replaying)
459             return;
460
461         this._startUpdatingCurrentTime();
462     },
463
464     _recordingTimesUpdated: function(event)
465     {
466         if (!this._waitingToResetCurrentTime)
467             return;
468
469         // Make the current time be the start time of the last added record. This is the best way
470         // currently to jump to the right period of time after recording starts.
471         // FIXME: If no activity is happening we can sit for a while until a record is added.
472         // We might want to have the backend send a "start" record to get current time moving.
473
474         for (var timeline of this._recording.timelines.values()) {
475             var lastRecord = timeline.records.lastValue;
476             if (!lastRecord)
477                 continue;
478             this._currentTime = Math.max(this._currentTime, lastRecord.startTime);
479         }
480
481         this._recording.removeEventListener(WebInspector.TimelineRecording.Event.TimesUpdated, this._recordingTimesUpdated, this);
482         this._waitingToResetCurrentTime = false;
483     },
484
485     _clearTimeline: function(event)
486     {
487         this._recording.reset();
488     },
489
490     _timelineAdded: function(timelineOrEvent)
491     {
492         var timeline = timelineOrEvent;
493         if (!(timeline instanceof WebInspector.Timeline))
494             timeline = timelineOrEvent.data.timeline;
495
496         console.assert(timeline instanceof WebInspector.Timeline, timeline);
497         console.assert(!this._timelineViewMap.has(timeline), timeline);
498
499         this._timelineViewMap.set(timeline, new WebInspector.ContentView(timeline));
500
501         var pathComponent = new WebInspector.HierarchicalPathComponent(timeline.displayName, timeline.iconClassName, timeline);
502         pathComponent.addEventListener(WebInspector.HierarchicalPathComponent.Event.SiblingWasSelected, this._pathComponentSelected, this);
503         this._pathComponentMap.set(timeline, pathComponent);
504
505         this._timelineCountChanged();
506     },
507
508     _timelineRemoved: function(event)
509     {
510         var timeline = event.data.timeline;
511         console.assert(timeline instanceof WebInspector.Timeline, timeline);
512         console.assert(this._timelineViewMap.has(timeline), timeline);
513
514         var timelineView = this._timelineViewMap.take(timeline);
515         if (this.currentTimelineView === timelineView)
516             this.showOverviewTimelineView();
517
518         this._pathComponentMap.delete(timeline);
519
520         this._timelineCountChanged();
521     },
522
523     _timelineCountChanged: function()
524     {
525         var previousPathComponent = null;
526         for (var pathComponent of this._pathComponentMap.values()) {
527             if (previousPathComponent) {
528                 previousPathComponent.nextSibling = pathComponent;
529                 pathComponent.previousSibling = previousPathComponent;
530             }
531
532             previousPathComponent = pathComponent;
533         }
534
535         var timelineCount = this._recording.timelines.size;
536         const timelineHeight = 36;
537         const extraOffset = 22;
538         this._timelineOverview.element.style.height = (timelineCount * timelineHeight + extraOffset) + "px";
539         this._contentViewContainer.element.style.top = (timelineCount * timelineHeight + extraOffset) + "px";
540     },
541
542     _recordingReset: function(event)
543     {
544         this._currentTime = NaN;
545
546         if (!this._updating) {
547             // Force the time ruler and views to reset to 0.
548             this._startTimeNeedsReset = true;
549             this._updateTimes(0, 0, 0);
550         }
551
552         this._lastUpdateTimestamp = NaN;
553         this._startTimeNeedsReset = true;
554
555         this._recording.removeEventListener(WebInspector.TimelineRecording.Event.TimesUpdated, this._recordingTimesUpdated, this);
556         this._waitingToResetCurrentTime = false;
557
558         this._timelineOverview.reset();
559         this._overviewTimelineView.reset();
560         for (var timelineView of this._timelineViewMap.values())
561             timelineView.reset();
562     },
563
564     _recordingUnloaded: function(event)
565     {
566         console.assert(!this._updating);
567
568         WebInspector.timelineManager.removeEventListener(WebInspector.TimelineManager.Event.CapturingStarted, this._capturingStarted, this);
569         WebInspector.timelineManager.removeEventListener(WebInspector.TimelineManager.Event.CapturingStopped, this._capturingStopped, this);
570     },
571
572     _timeRangeSelectionChanged: function(event)
573     {
574         if (this.currentTimelineView) {
575             this.currentTimelineView.startTime = this._timelineOverview.selectionStartTime;
576             this.currentTimelineView.endTime = this._timelineOverview.selectionStartTime + this._timelineOverview.selectionDuration;
577         }
578
579         // Delay until the next frame to stay in sync with the current timeline view's time-based layout changes.
580         requestAnimationFrame(function() {
581             var selectedTreeElement = this.currentTimelineView && this.currentTimelineView.navigationSidebarTreeOutline ? this.currentTimelineView.navigationSidebarTreeOutline.selectedTreeElement : null;
582             var selectionWasHidden = selectedTreeElement && selectedTreeElement.hidden;
583
584             WebInspector.timelineSidebarPanel.updateFilter();
585
586             if (selectedTreeElement && selectedTreeElement.hidden !== selectionWasHidden)
587                 this.dispatchEventToListeners(WebInspector.ContentView.Event.SelectionPathComponentsDidChange);
588         }.bind(this));
589     }
590 };