Web Inspector: support adding and removing timelines to the timeline sidebar panel...
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / TimelineRecordingContentView.js
1 /*
2  * Copyright (C) 2013 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WebInspector.TimelineRecordingContentView = function(recording)
27 {
28     WebInspector.ContentView.call(this, recording);
29
30     this._recording = recording;
31
32     this.element.classList.add(WebInspector.TimelineRecordingContentView.StyleClassName);
33
34     this._timelineOverview = new WebInspector.TimelineOverview(this._recording);
35     this._timelineOverview.addEventListener(WebInspector.TimelineOverview.Event.TimeRangeSelectionChanged, this._timeRangeSelectionChanged, this);
36     this.element.appendChild(this._timelineOverview.element);
37
38     this._viewContainerElement = document.createElement("div");
39     this._viewContainerElement.classList.add(WebInspector.TimelineRecordingContentView.ViewContainerStyleClassName);
40     this.element.appendChild(this._viewContainerElement);
41
42     var trashImage;
43     if (WebInspector.Platform.isLegacyMacOS)
44         trashImage = {src: "Images/Legacy/NavigationItemTrash.svg", width: 16, height: 16};
45     else
46         trashImage = {src: "Images/NavigationItemTrash.svg", width: 15, height: 15};
47
48     this._clearTimelineNavigationItem = new WebInspector.ButtonNavigationItem("clear-timeline", WebInspector.UIString("Clear Timeline"), trashImage.src, trashImage.width, trashImage.height);
49     this._clearTimelineNavigationItem.addEventListener(WebInspector.ButtonNavigationItem.Event.Clicked, this._clearTimeline, this);
50
51     this._overviewTimelineView = new WebInspector.OverviewTimelineView(recording);
52     this._overviewTimelineView.secondsPerPixel = this._timelineOverview.secondsPerPixel;
53
54     this._timelineViewMap = new Map;
55     this._pathComponentMap = new Map;
56
57     this._currentTimelineView = null;
58     this._currentTimelineViewIdentifier = null;
59
60     this._updating = false;
61     this._currentTime = NaN;
62     this._lastUpdateTimestamp = NaN;
63     this._startTimeNeedsReset = true;
64
65     this._recording.addEventListener(WebInspector.TimelineRecording.Event.TimelineAdded, this._timelineAdded, this);
66     this._recording.addEventListener(WebInspector.TimelineRecording.Event.TimelineRemoved, this._timelineRemoved, this);
67     this._recording.addEventListener(WebInspector.TimelineRecording.Event.Reset, this._recordingReset, this);
68     this._recording.addEventListener(WebInspector.TimelineRecording.Event.Unloaded, this._recordingUnloaded, this);
69
70     WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.Event.CapturingStarted, this._capturingStarted, this);
71     WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.Event.CapturingStopped, this._capturingStopped, this);
72
73     WebInspector.debuggerManager.addEventListener(WebInspector.DebuggerManager.Event.Paused, this._debuggerPaused, this);
74     WebInspector.debuggerManager.addEventListener(WebInspector.DebuggerManager.Event.Resumed, this._debuggerResumed, this);
75
76     for (var timeline of this._recording.timelines.values())
77         this._timelineAdded(timeline);
78
79     this.showOverviewTimelineView();
80 };
81
82 WebInspector.TimelineRecordingContentView.StyleClassName = "timeline-recording";
83 WebInspector.TimelineRecordingContentView.ViewContainerStyleClassName = "view-container";
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._showTimelineView(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._showTimelineView(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         var pathComponents = [];
123         if (this._currentTimelineView.representedObject instanceof WebInspector.Timeline)
124             pathComponents.push(this._pathComponentMap.get(this._currentTimelineView.representedObject));
125         pathComponents = pathComponents.concat(this._currentTimelineView.selectionPathComponents || []);
126         return pathComponents;
127     },
128
129     get navigationItems()
130     {
131         return [this._clearTimelineNavigationItem];
132     },
133
134     get currentTimelineView()
135     {
136         return this._currentTimelineView;
137     },
138
139     shown: function()
140     {
141         if (!this._currentTimelineView)
142             return;
143
144         this._timelineOverview.shown();
145         this._currentTimelineView.shown();
146         this._clearTimelineNavigationItem.enabled = this._recording.isWritable();
147
148         if (!this._updating && WebInspector.timelineManager.activeRecording === this._recording && WebInspector.timelineManager.isCapturing())
149             this._startUpdatingCurrentTime();
150     },
151
152     hidden: function()
153     {
154         if (!this._currentTimelineView)
155             return;
156
157         this._timelineOverview.hidden();
158         this._currentTimelineView.hidden();
159
160         if (this._updating)
161             this._stopUpdatingCurrentTime();
162     },
163
164     filterDidChange: function()
165     {
166         if (!this._currentTimelineView)
167             return;
168
169         this._currentTimelineView.filterDidChange();
170     },
171
172     updateLayout: function()
173     {
174         this._timelineOverview.updateLayoutForResize();
175
176         if (!this._currentTimelineView)
177             return;
178
179         this._currentTimelineView.updateLayout();
180     },
181
182     saveToCookie: function(cookie)
183     {
184         cookie.type = WebInspector.ContentViewCookieType.Timelines;
185
186         if (!this._currentTimelineView || this._currentTimelineView === this._overviewTimelineView)
187             cookie[WebInspector.TimelineRecordingContentView.SelectedTimelineTypeCookieKey] = WebInspector.TimelineRecordingContentView.OverviewTimelineViewCookieValue;
188         else
189             cookie[WebInspector.TimelineRecordingContentView.SelectedTimelineTypeCookieKey] = this._currentTimelineView.representedObject.type;
190     },
191
192     restoreFromCookie: function(cookie)
193     {
194         var timelineType = cookie[WebInspector.TimelineRecordingContentView.SelectedTimelineTypeCookieKey];
195
196         if (timelineType === WebInspector.TimelineRecordingContentView.OverviewTimelineViewCookieValue)
197             this.showOverviewTimelineView();
198         else
199             this.showTimelineViewForTimeline(this.representedObject.timelines.get(timelineType));
200     },
201
202     matchTreeElementAgainstCustomFilters: function(treeElement)
203     {
204         if (this._currentTimelineView && !this._currentTimelineView.matchTreeElementAgainstCustomFilters(treeElement))
205             return false;
206
207         var startTime = this._timelineOverview.selectionStartTime;
208         var endTime = this._timelineOverview.selectionStartTime + this._timelineOverview.selectionDuration;
209         var currentTime = this._currentTime || this._recording.startTime;
210
211         function checkTimeBounds(itemStartTime, itemEndTime)
212         {
213             itemStartTime = itemStartTime || currentTime;
214             itemEndTime = itemEndTime || currentTime;
215
216             return startTime <= itemEndTime && itemStartTime <= endTime;
217         }
218
219         if (treeElement instanceof WebInspector.ResourceTreeElement) {
220             var resource = treeElement.resource;
221             return checkTimeBounds(resource.requestSentTimestamp, resource.finishedOrFailedTimestamp);
222         }
223
224         if (treeElement instanceof WebInspector.SourceCodeTimelineTreeElement) {
225             var sourceCodeTimeline = treeElement.sourceCodeTimeline;
226
227             // Do a quick check of the timeline bounds before we check each record.
228             if (!checkTimeBounds(sourceCodeTimeline.startTime, sourceCodeTimeline.endTime))
229                 return false;
230
231             for (var record of sourceCodeTimeline.records) {
232                 if (checkTimeBounds(record.startTime, record.endTime))
233                     return true;
234             }
235
236             return false;
237         }
238
239         if (treeElement instanceof WebInspector.ProfileNodeTreeElement) {
240             var profileNode = treeElement.profileNode;
241             for (var call of profileNode.calls) {
242                 if (checkTimeBounds(call.startTime, call.endTime))
243                     return true;
244             }
245
246             return false;
247         }
248
249         if (treeElement instanceof WebInspector.TimelineRecordTreeElement) {
250             var record = treeElement.record;
251             return checkTimeBounds(record.startTime, record.endTime);
252         }
253
254         console.error("Unknown TreeElement, can't filter by time.");
255         return true;
256     },
257
258     // Private
259
260     _pathComponentSelected: function(event)
261     {
262         WebInspector.timelineSidebarPanel.showTimelineViewForTimeline(event.data.pathComponent.representedObject);
263     },
264
265     _timelineViewSelectionPathComponentsDidChange: function()
266     {
267         this.dispatchEventToListeners(WebInspector.ContentView.Event.SelectionPathComponentsDidChange);
268     },
269
270     _showTimelineView: function(timelineView)
271     {
272         console.assert(timelineView instanceof WebInspector.TimelineView);
273         console.assert(timelineView.representedObject === this._recording || this._recording.timelines.has(timelineView.representedObject.type));
274
275         // If the content view is shown and then hidden, we must reattach the content tree outline and timeline view.
276         if (timelineView.visible && timelineView === this._currentTimelineView)
277             return;
278
279         if (this._currentTimelineView) {
280             this._currentTimelineView.removeEventListener(WebInspector.TimelineView.Event.SelectionPathComponentsDidChange, this._timelineViewSelectionPathComponentsDidChange, this);
281
282             this._currentTimelineView.hidden();
283             this._currentTimelineView.element.remove();
284         }
285
286         this._currentTimelineView = timelineView;
287
288         WebInspector.timelineSidebarPanel.contentTreeOutline = timelineView && timelineView.navigationSidebarTreeOutline;
289         WebInspector.timelineSidebarPanel.contentTreeOutlineLabel = timelineView && timelineView.navigationSidebarTreeOutlineLabel;
290
291         if (this._currentTimelineView) {
292             this._currentTimelineView.addEventListener(WebInspector.TimelineView.Event.SelectionPathComponentsDidChange, this._timelineViewSelectionPathComponentsDidChange, this);
293
294             this._viewContainerElement.appendChild(this._currentTimelineView.element);
295
296             this._currentTimelineView.startTime = this._timelineOverview.selectionStartTime;
297             this._currentTimelineView.endTime = this._timelineOverview.selectionStartTime + this._timelineOverview.selectionDuration;
298             this._currentTimelineView.currentTime = this._currentTime;
299
300             this._currentTimelineView.shown();
301             this._currentTimelineView.updateLayout();
302         }
303
304         this.dispatchEventToListeners(WebInspector.ContentView.Event.SelectionPathComponentsDidChange);
305     },
306
307     _update: function(timestamp)
308     {
309         if (this._waitingToResetCurrentTime) {
310             requestAnimationFrame(this._updateCallback);
311             return;
312         }
313
314         var startTime = this._recording.startTime;
315         var currentTime = this._currentTime || startTime;
316         var endTime = this._recording.endTime;
317         var timespanSinceLastUpdate = (timestamp - this._lastUpdateTimestamp) / 1000 || 0;
318
319         currentTime += timespanSinceLastUpdate;
320
321         this._updateTimes(startTime, currentTime, endTime);
322
323         // Only stop updating if the current time is greater than the end time.
324         if (!this._updating && currentTime >= endTime) {
325             this._lastUpdateTimestamp = NaN;
326             return;
327         }
328
329         this._lastUpdateTimestamp = timestamp;
330
331         requestAnimationFrame(this._updateCallback);
332     },
333
334     _updateTimes: function(startTime, currentTime, endTime)
335     {
336         if (this._startTimeNeedsReset && !isNaN(startTime)) {
337             var selectionOffset = this._timelineOverview.selectionStartTime - this._timelineOverview.startTime;
338
339             this._timelineOverview.startTime = startTime;
340             this._timelineOverview.selectionStartTime = startTime + selectionOffset;
341
342             this._overviewTimelineView.zeroTime = startTime;
343             for (var timelineView of this._timelineViewMap.values())
344                 timelineView.zeroTime = startTime;
345
346             delete this._startTimeNeedsReset;
347         }
348
349         this._timelineOverview.endTime = Math.max(endTime, currentTime);
350
351         this._currentTime = currentTime;
352         this._timelineOverview.currentTime = currentTime;
353         this._currentTimelineView.currentTime = currentTime;
354
355         WebInspector.timelineSidebarPanel.updateFilter();
356
357         // Force a layout now since we are already in an animation frame and don't need to delay it until the next.
358         this._timelineOverview.updateLayoutIfNeeded();
359         this._currentTimelineView.updateLayoutIfNeeded();
360     },
361
362     _startUpdatingCurrentTime: function()
363     {
364         console.assert(!this._updating);
365         if (this._updating)
366             return;
367
368         if (!isNaN(this._currentTime)) {
369             // We have a current time already, so we likely need to jump into the future to a better current time.
370             // This happens when you stop and later restart recording.
371             console.assert(!this._waitingToResetCurrentTime);
372             this._waitingToResetCurrentTime = true;
373             this._recording.addEventListener(WebInspector.TimelineRecording.Event.TimesUpdated, this._recordingTimesUpdated, this);
374         }
375
376         this._updating = true;
377
378         if (!this._updateCallback)
379             this._updateCallback = this._update.bind(this);
380
381         requestAnimationFrame(this._updateCallback);
382     },
383
384     _stopUpdatingCurrentTime: function()
385     {
386         console.assert(this._updating);
387         this._updating = false;
388
389         if (this._waitingToResetCurrentTime) {
390             // Did not get any event while waiting for the current time, but we should stop waiting.
391             this._recording.removeEventListener(WebInspector.TimelineRecording.Event.TimesUpdated, this._recordingTimesUpdated, this);
392             this._waitingToResetCurrentTime = false;
393         }
394     },
395
396     _capturingStarted: function(event)
397     {
398         this._startUpdatingCurrentTime();
399     },
400
401     _capturingStopped: function(event)
402     {
403         if (this._updating)
404             this._stopUpdatingCurrentTime();
405     },
406
407     _debuggerPaused: function(event)
408     {
409         if (WebInspector.replayManager.sessionState === WebInspector.ReplayManager.SessionState.Replaying)
410             return;
411
412         this._stopUpdatingCurrentTime();
413     },
414
415     _debuggerResumed: function(event)
416     {
417         if (WebInspector.replayManager.sessionState === WebInspector.ReplayManager.SessionState.Replaying)
418             return;
419
420         this._startUpdatingCurrentTime();
421     },
422
423     _recordingTimesUpdated: function(event)
424     {
425         if (!this._waitingToResetCurrentTime)
426             return;
427
428         // Make the current time be the start time of the last added record. This is the best way
429         // currently to jump to the right period of time after recording starts.
430         // FIXME: If no activity is happening we can sit for a while until a record is added.
431         // We might want to have the backend send a "start" record to get current time moving.
432
433         for (var timeline of this._recording.timelines.values()) {
434             var lastRecord = timeline.records.lastValue;
435             if (!lastRecord)
436                 continue;
437             this._currentTime = Math.max(this._currentTime, lastRecord.startTime);
438         }
439
440         this._recording.removeEventListener(WebInspector.TimelineRecording.Event.TimesUpdated, this._recordingTimesUpdated, this);
441         this._waitingToResetCurrentTime = false;
442     },
443
444     _clearTimeline: function(event)
445     {
446         this._recording.reset();
447     },
448
449     _timelineAdded: function(timelineOrEvent)
450     {
451         var timeline = timelineOrEvent;
452         if (!(timeline instanceof WebInspector.Timeline))
453             timeline = timelineOrEvent.data.timeline;
454
455         console.assert(timeline instanceof WebInspector.Timeline, timeline);
456         console.assert(!this._timelineViewMap.has(timeline), timeline);
457
458         this._timelineViewMap.set(timeline, new WebInspector.TimelineView(timeline));
459
460         var pathComponent = new WebInspector.HierarchicalPathComponent(timeline.displayName, timeline.iconClassName, timeline);
461         pathComponent.addEventListener(WebInspector.HierarchicalPathComponent.Event.SiblingWasSelected, this._pathComponentSelected, this);
462         this._pathComponentMap.set(timeline, pathComponent);
463
464         this._timelineCountChanged();
465     },
466
467     _timelineRemoved: function(event)
468     {
469         var timeline = event.data.timeline;
470         console.assert(timeline instanceof WebInspector.Timeline, timeline);
471         console.assert(this._timelineViewMap.has(timeline), timeline);
472
473         var timelineView = this._timelineViewMap.take(timeline);
474         if (this._currentTimelineView === timelineView)
475             this.showOverviewTimelineView();
476
477         this._pathComponentMap.delete(timeline);
478
479         this._timelineCountChanged();
480     },
481
482     _timelineCountChanged: function()
483     {
484         var previousPathComponent = null;
485         for (var pathComponent of this._pathComponentMap.values()) {
486             if (previousPathComponent) {
487                 previousPathComponent.nextSibling = pathComponent;
488                 pathComponent.previousSibling = previousPathComponent;
489             }
490
491             previousPathComponent = pathComponent;
492         }
493
494         var timelineCount = this._recording.timelines.size;
495         const timelineHeight = 36;
496         const extraOffset = 22;
497         this._timelineOverview.element.style.height = (timelineCount * timelineHeight + extraOffset) + "px";
498         this._viewContainerElement.style.top = (timelineCount * timelineHeight + extraOffset) + "px";
499     },
500
501     _recordingReset: function(event)
502     {
503         this._currentTime = NaN;
504
505         if (!this._updating) {
506             // Force the time ruler and views to reset to 0.
507             this._startTimeNeedsReset = true;
508             this._updateTimes(0, 0, 0);
509         }
510
511         this._lastUpdateTimestamp = NaN;
512         this._startTimeNeedsReset = true;
513
514         this._recording.removeEventListener(WebInspector.TimelineRecording.Event.TimesUpdated, this._recordingTimesUpdated, this);
515         this._waitingToResetCurrentTime = false;
516
517         this._timelineOverview.reset();
518         this._overviewTimelineView.reset();
519         for (var timelineView of this._timelineViewMap.values())
520             timelineView.reset();
521     },
522
523     _recordingUnloaded: function(event)
524     {
525         console.assert(!this._updating);
526
527         WebInspector.timelineManager.removeEventListener(WebInspector.TimelineManager.Event.CapturingStarted, this._capturingStarted, this);
528         WebInspector.timelineManager.removeEventListener(WebInspector.TimelineManager.Event.CapturingStopped, this._capturingStopped, this);
529     },
530
531     _timeRangeSelectionChanged: function(event)
532     {
533         this._currentTimelineView.startTime = this._timelineOverview.selectionStartTime;
534         this._currentTimelineView.endTime = this._timelineOverview.selectionStartTime + this._timelineOverview.selectionDuration;
535
536         // Delay until the next frame to stay in sync with the current timeline view's time-based layout changes.
537         requestAnimationFrame(function() {
538             var selectedTreeElement = this._currentTimelineView && this._currentTimelineView.navigationSidebarTreeOutline ? this._currentTimelineView.navigationSidebarTreeOutline.selectedTreeElement : null;
539             var selectionWasHidden = selectedTreeElement && selectedTreeElement.hidden;
540
541             WebInspector.timelineSidebarPanel.updateFilter();
542
543             if (selectedTreeElement && selectedTreeElement.hidden !== selectionWasHidden)
544                 this.dispatchEventToListeners(WebInspector.ContentView.Event.SelectionPathComponentsDidChange);
545         }.bind(this));
546     }
547 };