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