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
26WebInspector.TimelineContentView = function(recording)
27{
28    WebInspector.ContentView.call(this, recording);
29
30    this._recording = recording;
31
32    this.element.classList.add(WebInspector.TimelineContentView.StyleClassName);
33
34    this._discreteTimelineOverviewGraphMap = new Map;
35    this._discreteTimelineOverviewGraphMap.set(WebInspector.TimelineRecord.Type.Network, new WebInspector.NetworkTimelineOverviewGraph(recording));
36    this._discreteTimelineOverviewGraphMap.set(WebInspector.TimelineRecord.Type.Layout, new WebInspector.LayoutTimelineOverviewGraph(recording));
37    this._discreteTimelineOverviewGraphMap.set(WebInspector.TimelineRecord.Type.Script, new WebInspector.ScriptTimelineOverviewGraph(recording));
38
39    this._timelineOverview = new WebInspector.TimelineOverview(this._discreteTimelineOverviewGraphMap);
40    this._timelineOverview.addEventListener(WebInspector.TimelineOverview.Event.TimeRangeSelectionChanged, this._timeRangeSelectionChanged, this);
41    this.element.appendChild(this._timelineOverview.element);
42
43    this._viewContainer = document.createElement("div");
44    this._viewContainer.classList.add(WebInspector.TimelineContentView.ViewContainerStyleClassName);
45    this.element.appendChild(this._viewContainer);
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);
57
58    this._discreteTimelineViewMap = new Map;
59    this._discreteTimelineViewMap.set(WebInspector.TimelineRecord.Type.Network, new WebInspector.NetworkTimelineView(recording));
60    this._discreteTimelineViewMap.set(WebInspector.TimelineRecord.Type.Layout, new WebInspector.LayoutTimelineView(recording));
61    this._discreteTimelineViewMap.set(WebInspector.TimelineRecord.Type.Script, new WebInspector.ScriptTimelineView(recording));
62
63    function createPathComponent(displayName, className, representedObject)
64    {
65        var pathComponent = new WebInspector.HierarchicalPathComponent(displayName, className, representedObject);
66        pathComponent.addEventListener(WebInspector.HierarchicalPathComponent.Event.SiblingWasSelected, this._pathComponentSelected, this);
67        return pathComponent;
68    }
69
70    this._pathComponentMap = new Map;
71    this._pathComponentMap.set(WebInspector.TimelineRecord.Type.Network, createPathComponent.call(this, WebInspector.UIString("Network Requests"), WebInspector.TimelineSidebarPanel.NetworkIconStyleClass, WebInspector.TimelineRecord.Type.Network));
72    this._pathComponentMap.set(WebInspector.TimelineRecord.Type.Layout, createPathComponent.call(this, WebInspector.UIString("Layout & Rendering"), WebInspector.TimelineSidebarPanel.ColorsIconStyleClass, WebInspector.TimelineRecord.Type.Layout));
73    this._pathComponentMap.set(WebInspector.TimelineRecord.Type.Script, createPathComponent.call(this, WebInspector.UIString("JavaScript & Events"), WebInspector.TimelineSidebarPanel.ScriptIconStyleClass, WebInspector.TimelineRecord.Type.Script));
74
75    var previousPathComponent = null;
76    for (var pathComponent of this._pathComponentMap.values()) {
77        if (previousPathComponent) {
78            previousPathComponent.nextSibling = pathComponent;
79            pathComponent.previousSibling = previousPathComponent;
80        }
81
82        previousPathComponent = pathComponent;
83    }
84
85    this._currentTimelineView = null;
86    this._currentTimelineViewIdentifier = null;
87
88    this._updating = false;
89    this._currentTime = NaN;
90    this._lastUpdateTimestamp = NaN;
91    this._startTimeNeedsReset = true;
92
93    recording.addEventListener(WebInspector.TimelineRecording.Event.Reset, this._recordingReset, this);
94
95    WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.Event.CapturingStarted, this._capturingStarted, this);
96    WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.Event.CapturingStopped, this._capturingStopped, this);
97
98    this.showOverviewTimelineView();
99};
100
101WebInspector.TimelineContentView.StyleClassName = "timeline";
102WebInspector.TimelineContentView.ViewContainerStyleClassName = "view-container";
103
104WebInspector.TimelineContentView.prototype = {
105    constructor: WebInspector.TimelineContentView,
106    __proto__: WebInspector.ContentView.prototype,
107
108    // Public
109
110    showOverviewTimelineView: function()
111    {
112        this._showTimelineView(this._overviewTimelineView);
113    },
114
115    showTimelineView: function(identifier)
116    {
117        console.assert(this._discreteTimelineViewMap.has(identifier));
118        if (!this._discreteTimelineViewMap.has(identifier))
119            return;
120
121        this._showTimelineView(this._discreteTimelineViewMap.get(identifier), identifier);
122    },
123
124    get allowedNavigationSidebarPanels()
125    {
126        return ["timeline"];
127    },
128
129    get supportsSplitContentBrowser()
130    {
131        // The layout of the overview and split content browser don't work well.
132        return false;
133    },
134
135    get selectionPathComponents()
136    {
137        var pathComponents = this._currentTimelineViewIdentifier ? [this._pathComponentMap.get(this._currentTimelineViewIdentifier)] : [];
138        pathComponents = pathComponents.concat(this._currentTimelineView.selectionPathComponents || []);
139        return pathComponents;
140    },
141
142    get navigationItems()
143    {
144        return [this._clearTimelineNavigationItem];
145    },
146
147    shown: function()
148    {
149        if (!this._currentTimelineView)
150            return;
151
152        this._currentTimelineView.shown();
153    },
154
155    hidden: function()
156    {
157        if (!this._currentTimelineView)
158            return;
159
160        this._currentTimelineView.hidden();
161    },
162
163    updateLayout: function()
164    {
165        this._timelineOverview.updateLayoutForResize();
166
167        if (!this._currentTimelineView)
168            return;
169
170        this._currentTimelineView.updateLayout();
171    },
172
173    matchTreeElementAgainstCustomFilters: function(treeElement)
174    {
175        if (this._currentTimelineView && !this._currentTimelineView.matchTreeElementAgainstCustomFilters(treeElement))
176            return false;
177
178        var startTime = this._timelineOverview.selectionStartTime;
179        var endTime = this._timelineOverview.selectionStartTime + this._timelineOverview.selectionDuration;
180        var currentTime = this._currentTime || this._recording.startTime;
181
182        function checkTimeBounds(itemStartTime, itemEndTime)
183        {
184            itemStartTime = itemStartTime || currentTime;
185            itemEndTime = itemEndTime || currentTime;
186
187            return startTime <= itemEndTime && itemStartTime <= endTime;
188        }
189
190        if (treeElement instanceof WebInspector.ResourceTreeElement) {
191            var resource = treeElement.resource;
192            return checkTimeBounds(resource.requestSentTimestamp, resource.finishedOrFailedTimestamp);
193        }
194
195        if (treeElement instanceof WebInspector.SourceCodeTimelineTreeElement) {
196            var sourceCodeTimeline = treeElement.sourceCodeTimeline;
197
198            // Do a quick check of the timeline bounds before we check each record.
199            if (!checkTimeBounds(sourceCodeTimeline.startTime, sourceCodeTimeline.endTime))
200                return false;
201
202            for (var record of sourceCodeTimeline.records) {
203                if (checkTimeBounds(record.startTime, record.endTime))
204                    return true;
205            }
206
207            return false;
208        }
209
210        if (treeElement instanceof WebInspector.ProfileNodeTreeElement) {
211            var profileNode = treeElement.profileNode;
212            for (var call of profileNode.calls) {
213                if (checkTimeBounds(call.startTime, call.endTime))
214                    return true;
215            }
216
217            return false;
218        }
219
220        if (treeElement instanceof WebInspector.TimelineRecordTreeElement) {
221            var record = treeElement.record;
222            return checkTimeBounds(record.startTime, record.endTime);
223        }
224
225        console.error("Unknown TreeElement, can't filter by time.");
226        return true;
227    },
228
229    // Private
230
231    _pathComponentSelected: function(event)
232    {
233        WebInspector.timelineSidebarPanel.showTimelineView(event.data.pathComponent.representedObject);
234    },
235
236    _timelineViewSelectionPathComponentsDidChange: function()
237    {
238        this.dispatchEventToListeners(WebInspector.ContentView.Event.SelectionPathComponentsDidChange);
239    },
240
241    _showTimelineView: function(timelineView, identifier)
242    {
243        console.assert(timelineView instanceof WebInspector.TimelineView);
244
245        if (this._currentTimelineView === timelineView)
246            return;
247
248        if (this._currentTimelineView) {
249            this._currentTimelineView.removeEventListener(WebInspector.TimelineView.Event.SelectionPathComponentsDidChange, this._timelineViewSelectionPathComponentsDidChange, this);
250
251            this._currentTimelineView.hidden();
252            this._currentTimelineView.element.remove();
253        }
254
255        this._currentTimelineView = timelineView;
256        this._currentTimelineViewIdentifier = identifier || null;
257
258        WebInspector.timelineSidebarPanel.contentTreeOutline = timelineView && timelineView.navigationSidebarTreeOutline;
259        WebInspector.timelineSidebarPanel.contentTreeOutlineLabel = timelineView && timelineView.navigationSidebarTreeOutlineLabel;
260
261        if (this._currentTimelineView) {
262            this._currentTimelineView.addEventListener(WebInspector.TimelineView.Event.SelectionPathComponentsDidChange, this._timelineViewSelectionPathComponentsDidChange, this);
263
264            this._viewContainer.appendChild(this._currentTimelineView.element);
265
266            this._currentTimelineView.startTime = this._timelineOverview.selectionStartTime;
267            this._currentTimelineView.endTime = this._timelineOverview.selectionStartTime + this._timelineOverview.selectionDuration;
268            this._currentTimelineView.currentTime = this._currentTime;
269
270            this._currentTimelineView.shown();
271            this._currentTimelineView.updateLayout();
272        }
273
274        this.dispatchEventToListeners(WebInspector.ContentView.Event.SelectionPathComponentsDidChange);
275    },
276
277    _update: function(timestamp)
278    {
279        if (this._waitingToResetCurrentTime) {
280            requestAnimationFrame(this._updateCallback);
281            return;
282        }
283
284        var startTime = this._recording.startTime;
285        var currentTime = this._currentTime || startTime;
286        var endTime = this._recording.endTime;
287        var timespanSinceLastUpdate = (timestamp - this._lastUpdateTimestamp) / 1000 || 0;
288
289        currentTime += timespanSinceLastUpdate;
290
291        this._updateTimes(startTime, currentTime, endTime);
292
293        // Only stop updating if the current time is greater than the end time.
294        if (!this._updating && currentTime >= endTime) {
295            this._lastUpdateTimestamp = NaN;
296            return;
297        }
298
299        this._lastUpdateTimestamp = timestamp;
300
301        requestAnimationFrame(this._updateCallback);
302    },
303
304    _updateTimes: function(startTime, currentTime, endTime)
305    {
306        if (this._startTimeNeedsReset && !isNaN(startTime)) {
307            var selectionOffset = this._timelineOverview.selectionStartTime - this._timelineOverview.startTime;
308
309            this._timelineOverview.startTime = startTime;
310            this._timelineOverview.selectionStartTime = startTime + selectionOffset;
311
312            this._overviewTimelineView.zeroTime = startTime;
313            for (var timelineView of this._discreteTimelineViewMap.values())
314                timelineView.zeroTime = startTime;
315
316            delete this._startTimeNeedsReset;
317        }
318
319        this._timelineOverview.endTime = Math.max(endTime, currentTime);
320
321        this._currentTime = currentTime;
322        this._timelineOverview.currentTime = currentTime;
323        this._currentTimelineView.currentTime = currentTime;
324
325        // Force a layout now since we are already in an animation frame and don't need to delay it until the next.
326        this._timelineOverview.updateLayoutIfNeeded();
327        this._currentTimelineView.updateLayoutIfNeeded();
328    },
329
330    _startUpdatingCurrentTime: function()
331    {
332        console.assert(!this._updating);
333        if (this._updating)
334            return;
335
336        if (!isNaN(this._currentTime)) {
337            // We have a current time already, so we likely need to jump into the future to a better current time.
338            // This happens when you stop and later restart recording.
339            console.assert(!this._waitingToResetCurrentTime);
340            this._waitingToResetCurrentTime = true;
341            this._recording.addEventListener(WebInspector.TimelineRecording.Event.TimesUpdated, this._recordingTimesUpdated, this);
342        }
343
344        this._updating = true;
345
346        if (!this._updateCallback)
347            this._updateCallback = this._update.bind(this);
348
349        requestAnimationFrame(this._updateCallback);
350    },
351
352    _stopUpdatingCurrentTime: function()
353    {
354        console.assert(this._updating);
355        this._updating = false;
356
357        if (this._waitingToResetCurrentTime) {
358            // Did not get any event while waiting for the current time, but we should stop waiting.
359            this._recording.removeEventListener(WebInspector.TimelineRecording.Event.TimesUpdated, this._recordingTimesUpdated, this);
360            this._waitingToResetCurrentTime = false;
361        }
362    },
363
364    _capturingStarted: function(event)
365    {
366        this._startUpdatingCurrentTime();
367    },
368
369    _capturingStopped: function(event)
370    {
371        this._stopUpdatingCurrentTime();
372    },
373
374    _recordingTimesUpdated: function(event)
375    {
376        if (!this._waitingToResetCurrentTime)
377            return;
378
379        // Make the current time be the start time of the last added record. This is the best way
380        // currently to jump to the right period of time after recording starts.
381        // FIXME: If no activity is happening we can sit for a while until a record is added.
382        // We might want to have the backend send a "start" record to get current time moving.
383
384        for (var timeline of this._recording.timelines.values()) {
385            var lastRecord = timeline.records.lastValue;
386            if (!lastRecord)
387                continue;
388            this._currentTime = Math.max(this._currentTime, lastRecord.startTime);
389        }
390
391        this._recording.removeEventListener(WebInspector.TimelineRecording.Event.TimesUpdated, this._recordingTimesUpdated, this);
392        this._waitingToResetCurrentTime = false;
393    },
394
395    _clearTimeline: function(event)
396    {
397        this._recording.reset();
398    },
399
400    _recordingReset: function(event)
401    {
402        this._currentTime = NaN;
403
404        if (!this._updating) {
405            // Force the time ruler and views to reset to 0.
406            this._startTimeNeedsReset = true;
407            this._updateTimes(0, 0, 0);
408        }
409
410        this._lastUpdateTimestamp = NaN;
411        this._startTimeNeedsReset = true;
412
413        this._recording.removeEventListener(WebInspector.TimelineRecording.Event.TimesUpdated, this._recordingTimesUpdated, this);
414        this._waitingToResetCurrentTime = false;
415
416        this._overviewTimelineView.reset();
417        for (var timelineView of this._discreteTimelineViewMap.values())
418            timelineView.reset();
419
420        for (var timelineOverviewGraph of this._discreteTimelineOverviewGraphMap.values())
421            timelineOverviewGraph.reset();
422    },
423
424    _timeRangeSelectionChanged: function(event)
425    {
426        this._currentTimelineView.startTime = this._timelineOverview.selectionStartTime;
427        this._currentTimelineView.endTime = this._timelineOverview.selectionStartTime + this._timelineOverview.selectionDuration;
428
429        // Delay until the next frame to stay in sync with the current timeline view's time-based layout changes.
430        requestAnimationFrame(function() {
431            var selectedTreeElement = this._currentTimelineView && this._currentTimelineView.navigationSidebarTreeOutline ? this._currentTimelineView.navigationSidebarTreeOutline.selectedTreeElement : null;
432            var selectionWasHidden = selectedTreeElement && selectedTreeElement.hidden;
433
434            WebInspector.timelineSidebarPanel.updateFilter();
435
436            if (selectedTreeElement && selectedTreeElement.hidden !== selectionWasHidden)
437                this.dispatchEventToListeners(WebInspector.ContentView.Event.SelectionPathComponentsDidChange);
438        }.bind(this));
439    }
440};
441