Web Inspector: Make closing ContentViews more leak proof
[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         this._recording.removeEventListener(null, null, this);
192
193         WebInspector.timelineManager.removeEventListener(null, null, this);
194         WebInspector.debuggerManager.removeEventListener(null, null, this);
195         WebInspector.ContentView.removeEventListener(null, null, this);
196     },
197
198     canGoBack: function()
199     {
200         return this._contentViewContainer.canGoBack();
201     },
202
203     canGoForward: function()
204     {
205         return this._contentViewContainer.canGoForward();
206     },
207
208     goBack: function()
209     {
210         this._contentViewContainer.goBack();
211     },
212
213     goForward: function()
214     {
215         this._contentViewContainer.goForward();
216     },
217
218     updateLayout: function()
219     {
220         this._timelineOverview.updateLayoutForResize();
221
222         var currentContentView = this._contentViewContainer.currentContentView;
223         if (currentContentView)
224             currentContentView.updateLayout();
225     },
226
227     saveToCookie: function(cookie)
228     {
229         cookie.type = WebInspector.ContentViewCookieType.Timelines;
230
231         var currentContentView = this._contentViewContainer.currentContentView;
232         if (!currentContentView || currentContentView === this._overviewTimelineView)
233             cookie[WebInspector.TimelineRecordingContentView.SelectedTimelineTypeCookieKey] = WebInspector.TimelineRecordingContentView.OverviewTimelineViewCookieValue;
234         else if (currentContentView.representedObject instanceof WebInspector.Timeline)
235             cookie[WebInspector.TimelineRecordingContentView.SelectedTimelineTypeCookieKey] = this.currentTimelineView.representedObject.type;
236     },
237
238     restoreFromCookie: function(cookie)
239     {
240         var timelineType = cookie[WebInspector.TimelineRecordingContentView.SelectedTimelineTypeCookieKey];
241
242         if (timelineType === WebInspector.TimelineRecordingContentView.OverviewTimelineViewCookieValue)
243             this.showOverviewTimelineView();
244         else
245             this.showTimelineViewForTimeline(this.representedObject.timelines.get(timelineType));
246     },
247
248     filterDidChange: function()
249     {
250         if (!this.currentTimelineView)
251             return;
252
253         this.currentTimelineView.filterDidChange();
254     },
255
256     matchTreeElementAgainstCustomFilters: function(treeElement)
257     {
258         if (this.currentTimelineView && !this.currentTimelineView.matchTreeElementAgainstCustomFilters(treeElement))
259             return false;
260
261         var startTime = this._timelineOverview.selectionStartTime;
262         var endTime = this._timelineOverview.selectionStartTime + this._timelineOverview.selectionDuration;
263         var currentTime = this._currentTime || this._recording.startTime;
264
265         function checkTimeBounds(itemStartTime, itemEndTime)
266         {
267             itemStartTime = itemStartTime || currentTime;
268             itemEndTime = itemEndTime || currentTime;
269
270             return startTime <= itemEndTime && itemStartTime <= endTime;
271         }
272
273         if (treeElement instanceof WebInspector.ResourceTreeElement) {
274             var resource = treeElement.resource;
275             return checkTimeBounds(resource.requestSentTimestamp, resource.finishedOrFailedTimestamp);
276         }
277
278         if (treeElement instanceof WebInspector.SourceCodeTimelineTreeElement) {
279             var sourceCodeTimeline = treeElement.sourceCodeTimeline;
280
281             // Do a quick check of the timeline bounds before we check each record.
282             if (!checkTimeBounds(sourceCodeTimeline.startTime, sourceCodeTimeline.endTime))
283                 return false;
284
285             for (var record of sourceCodeTimeline.records) {
286                 if (checkTimeBounds(record.startTime, record.endTime))
287                     return true;
288             }
289
290             return false;
291         }
292
293         if (treeElement instanceof WebInspector.ProfileNodeTreeElement) {
294             var profileNode = treeElement.profileNode;
295             for (var call of profileNode.calls) {
296                 if (checkTimeBounds(call.startTime, call.endTime))
297                     return true;
298             }
299
300             return false;
301         }
302
303         if (treeElement instanceof WebInspector.TimelineRecordTreeElement) {
304             var record = treeElement.record;
305             return checkTimeBounds(record.startTime, record.endTime);
306         }
307
308         console.error("Unknown TreeElement, can't filter by time.");
309         return true;
310     },
311
312     // Private
313
314     _currentContentViewDidChange: function(event)
315     {
316         var timelineView = this.currentTimelineView;
317         if (timelineView) {
318             WebInspector.timelineSidebarPanel.contentTreeOutline = timelineView.navigationSidebarTreeOutline;
319             WebInspector.timelineSidebarPanel.contentTreeOutlineLabel = timelineView.navigationSidebarTreeOutlineLabel;
320
321             timelineView.startTime = this._timelineOverview.selectionStartTime;
322             timelineView.endTime = this._timelineOverview.selectionStartTime + this._timelineOverview.selectionDuration;
323             timelineView.currentTime = this._currentTime;
324         }
325
326         this.dispatchEventToListeners(WebInspector.ContentView.Event.SelectionPathComponentsDidChange);
327         this.dispatchEventToListeners(WebInspector.ContentView.Event.NavigationItemsDidChange);
328     },
329
330     _pathComponentSelected: function(event)
331     {
332         WebInspector.timelineSidebarPanel.showTimelineViewForTimeline(event.data.pathComponent.representedObject);
333     },
334
335     _contentViewSelectionPathComponentDidChange: function(event)
336     {
337         if (event.target !== this._contentViewContainer.currentContentView)
338             return;
339         this.dispatchEventToListeners(WebInspector.ContentView.Event.SelectionPathComponentsDidChange);
340     },
341
342     _contentViewSupplementalRepresentedObjectsDidChange: function(event)
343     {
344         if (event.target !== this._contentViewContainer.currentContentView)
345             return;
346         this.dispatchEventToListeners(WebInspector.ContentView.Event.SupplementalRepresentedObjectsDidChange);
347     },
348
349     _update: function(timestamp)
350     {
351         if (this._waitingToResetCurrentTime) {
352             requestAnimationFrame(this._updateCallback);
353             return;
354         }
355
356         var startTime = this._recording.startTime;
357         var currentTime = this._currentTime || startTime;
358         var endTime = this._recording.endTime;
359         var timespanSinceLastUpdate = (timestamp - this._lastUpdateTimestamp) / 1000 || 0;
360
361         currentTime += timespanSinceLastUpdate;
362
363         this._updateTimes(startTime, currentTime, endTime);
364
365         // Only stop updating if the current time is greater than the end time.
366         if (!this._updating && currentTime >= endTime) {
367             this._lastUpdateTimestamp = NaN;
368             return;
369         }
370
371         this._lastUpdateTimestamp = timestamp;
372
373         requestAnimationFrame(this._updateCallback);
374     },
375
376     _updateTimes: function(startTime, currentTime, endTime)
377     {
378         if (this._startTimeNeedsReset && !isNaN(startTime)) {
379             var selectionOffset = this._timelineOverview.selectionStartTime - this._timelineOverview.startTime;
380
381             this._timelineOverview.startTime = startTime;
382             this._timelineOverview.selectionStartTime = startTime + selectionOffset;
383
384             this._overviewTimelineView.zeroTime = startTime;
385             for (var timelineView of this._timelineViewMap.values())
386                 timelineView.zeroTime = startTime;
387
388             delete this._startTimeNeedsReset;
389         }
390
391         this._timelineOverview.endTime = Math.max(endTime, currentTime);
392
393         this._currentTime = currentTime;
394         this._timelineOverview.currentTime = currentTime;
395         if (this.currentTimelineView)
396             this.currentTimelineView.currentTime = currentTime;
397
398         WebInspector.timelineSidebarPanel.updateFilter();
399
400         // Force a layout now since we are already in an animation frame and don't need to delay it until the next.
401         this._timelineOverview.updateLayoutIfNeeded();
402         if (this.currentTimelineView)
403             this.currentTimelineView.updateLayoutIfNeeded();
404     },
405
406     _startUpdatingCurrentTime: function()
407     {
408         console.assert(!this._updating);
409         if (this._updating)
410             return;
411
412         if (!isNaN(this._currentTime)) {
413             // We have a current time already, so we likely need to jump into the future to a better current time.
414             // This happens when you stop and later restart recording.
415             console.assert(!this._waitingToResetCurrentTime);
416             this._waitingToResetCurrentTime = true;
417             this._recording.addEventListener(WebInspector.TimelineRecording.Event.TimesUpdated, this._recordingTimesUpdated, this);
418         }
419
420         this._updating = true;
421
422         if (!this._updateCallback)
423             this._updateCallback = this._update.bind(this);
424
425         requestAnimationFrame(this._updateCallback);
426     },
427
428     _stopUpdatingCurrentTime: function()
429     {
430         console.assert(this._updating);
431         this._updating = false;
432
433         if (this._waitingToResetCurrentTime) {
434             // Did not get any event while waiting for the current time, but we should stop waiting.
435             this._recording.removeEventListener(WebInspector.TimelineRecording.Event.TimesUpdated, this._recordingTimesUpdated, this);
436             this._waitingToResetCurrentTime = false;
437         }
438     },
439
440     _capturingStarted: function(event)
441     {
442         this._startUpdatingCurrentTime();
443     },
444
445     _capturingStopped: function(event)
446     {
447         if (this._updating)
448             this._stopUpdatingCurrentTime();
449     },
450
451     _debuggerPaused: function(event)
452     {
453         if (WebInspector.replayManager.sessionState === WebInspector.ReplayManager.SessionState.Replaying)
454             return;
455
456         this._stopUpdatingCurrentTime();
457     },
458
459     _debuggerResumed: function(event)
460     {
461         if (WebInspector.replayManager.sessionState === WebInspector.ReplayManager.SessionState.Replaying)
462             return;
463
464         this._startUpdatingCurrentTime();
465     },
466
467     _recordingTimesUpdated: function(event)
468     {
469         if (!this._waitingToResetCurrentTime)
470             return;
471
472         // Make the current time be the start time of the last added record. This is the best way
473         // currently to jump to the right period of time after recording starts.
474         // FIXME: If no activity is happening we can sit for a while until a record is added.
475         // We might want to have the backend send a "start" record to get current time moving.
476
477         for (var timeline of this._recording.timelines.values()) {
478             var lastRecord = timeline.records.lastValue;
479             if (!lastRecord)
480                 continue;
481             this._currentTime = Math.max(this._currentTime, lastRecord.startTime);
482         }
483
484         this._recording.removeEventListener(WebInspector.TimelineRecording.Event.TimesUpdated, this._recordingTimesUpdated, this);
485         this._waitingToResetCurrentTime = false;
486     },
487
488     _clearTimeline: function(event)
489     {
490         this._recording.reset();
491     },
492
493     _timelineAdded: function(timelineOrEvent)
494     {
495         var timeline = timelineOrEvent;
496         if (!(timeline instanceof WebInspector.Timeline))
497             timeline = timelineOrEvent.data.timeline;
498
499         console.assert(timeline instanceof WebInspector.Timeline, timeline);
500         if (!WebInspector.TimelineManager.shouldShowViewForTimeline(timeline))
501             return;
502
503         console.assert(!this._timelineViewMap.has(timeline), timeline);
504
505         this._timelineViewMap.set(timeline, new WebInspector.ContentView(timeline));
506
507         var pathComponent = new WebInspector.HierarchicalPathComponent(timeline.displayName, timeline.iconClassName, timeline);
508         pathComponent.addEventListener(WebInspector.HierarchicalPathComponent.Event.SiblingWasSelected, this._pathComponentSelected, this);
509         this._pathComponentMap.set(timeline, pathComponent);
510
511         this._timelineCountChanged();
512     },
513
514     _timelineRemoved: function(event)
515     {
516         var timeline = event.data.timeline;
517         console.assert(timeline instanceof WebInspector.Timeline, timeline);
518         if (!WebInspector.TimelineManager.shouldShowViewForTimeline(timeline))
519             return;
520
521         console.assert(this._timelineViewMap.has(timeline), timeline);
522
523         var timelineView = this._timelineViewMap.take(timeline);
524         if (this.currentTimelineView === timelineView)
525             this.showOverviewTimelineView();
526
527         this._pathComponentMap.delete(timeline);
528
529         this._timelineCountChanged();
530     },
531
532     _timelineCountChanged: function()
533     {
534         var previousPathComponent = null;
535         for (var pathComponent of this._pathComponentMap.values()) {
536             if (previousPathComponent) {
537                 previousPathComponent.nextSibling = pathComponent;
538                 pathComponent.previousSibling = previousPathComponent;
539             }
540
541             previousPathComponent = pathComponent;
542         }
543
544         var timelineCount = this._timelineViewMap.size;
545         const timelineHeight = 36;
546         const extraOffset = 22;
547         this._timelineOverview.element.style.height = (timelineCount * timelineHeight + extraOffset) + "px";
548         this._contentViewContainer.element.style.top = (timelineCount * timelineHeight + extraOffset) + "px";
549     },
550
551     _recordingReset: function(event)
552     {
553         this._currentTime = NaN;
554
555         if (!this._updating) {
556             // Force the time ruler and views to reset to 0.
557             this._startTimeNeedsReset = true;
558             this._updateTimes(0, 0, 0);
559         }
560
561         this._lastUpdateTimestamp = NaN;
562         this._startTimeNeedsReset = true;
563
564         this._recording.removeEventListener(WebInspector.TimelineRecording.Event.TimesUpdated, this._recordingTimesUpdated, this);
565         this._waitingToResetCurrentTime = false;
566
567         this._timelineOverview.reset();
568         this._overviewTimelineView.reset();
569         for (var timelineView of this._timelineViewMap.values())
570             timelineView.reset();
571     },
572
573     _recordingUnloaded: function(event)
574     {
575         console.assert(!this._updating);
576
577         WebInspector.timelineManager.removeEventListener(WebInspector.TimelineManager.Event.CapturingStarted, this._capturingStarted, this);
578         WebInspector.timelineManager.removeEventListener(WebInspector.TimelineManager.Event.CapturingStopped, this._capturingStopped, this);
579     },
580
581     _timeRangeSelectionChanged: function(event)
582     {
583         if (this.currentTimelineView) {
584             this.currentTimelineView.startTime = this._timelineOverview.selectionStartTime;
585             this.currentTimelineView.endTime = this._timelineOverview.selectionStartTime + this._timelineOverview.selectionDuration;
586
587             WebInspector.renderingFrameDetailsSidebarPanel.updateRangeSelection(this.currentTimelineView.startTime, this.currentTimelineView.endTime);
588         }
589
590         // Delay until the next frame to stay in sync with the current timeline view's time-based layout changes.
591         requestAnimationFrame(function() {
592             var selectedTreeElement = this.currentTimelineView && this.currentTimelineView.navigationSidebarTreeOutline ? this.currentTimelineView.navigationSidebarTreeOutline.selectedTreeElement : null;
593             var selectionWasHidden = selectedTreeElement && selectedTreeElement.hidden;
594
595             WebInspector.timelineSidebarPanel.updateFilter();
596
597             if (selectedTreeElement && selectedTreeElement.hidden !== selectionWasHidden)
598                 this.dispatchEventToListeners(WebInspector.ContentView.Event.SelectionPathComponentsDidChange);
599         }.bind(this));
600     }
601 };