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