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.TimelineManager = function() 27{ 28 WebInspector.Object.call(this); 29 30 WebInspector.Frame.addEventListener(WebInspector.Frame.Event.ProvisionalLoadStarted, this._startAutoCapturing, this); 31 WebInspector.Frame.addEventListener(WebInspector.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this); 32 WebInspector.Frame.addEventListener(WebInspector.Frame.Event.ResourceWasAdded, this._resourceWasAdded, this); 33 34 this._activeRecording = new WebInspector.TimelineRecording; 35 this._isCapturing = false; 36 37 this._boundStopCapturing = this.stopCapturing.bind(this); 38}; 39 40WebInspector.TimelineManager.Event = { 41 CapturingStarted: "timeline-manager-capturing-started", 42 CapturingStopped: "timeline-manager-capturing-stopped" 43}; 44 45WebInspector.TimelineManager.MaximumAutoRecordDuration = 90000; // 90 seconds 46WebInspector.TimelineManager.MaximumAutoRecordDurationAfterLoadEvent = 10000; // 10 seconds 47WebInspector.TimelineManager.DeadTimeRequiredToStopAutoRecordingEarly = 2000; // 2 seconds 48 49WebInspector.TimelineManager.prototype = { 50 constructor: WebInspector.TimelineManager, 51 52 // Public 53 54 get activeRecording() 55 { 56 return this._activeRecording; 57 }, 58 59 isCapturing: function() 60 { 61 return this._isCapturing; 62 }, 63 64 startCapturing: function() 65 { 66 TimelineAgent.start(); 67 68 // COMPATIBILITY (iOS 7): recordingStarted event did not exist yet. Start explicitly. 69 if (!TimelineAgent.hasEvent("recordingStarted")) 70 this.capturingStarted(); 71 }, 72 73 stopCapturing: function() 74 { 75 TimelineAgent.stop(); 76 77 // NOTE: Always stop immediately instead of waiting for a Timeline.recordingStopped event. 78 // This way the UI feels as responsive to a stop as possible. 79 this.capturingStopped(); 80 }, 81 82 capturingStarted: function() 83 { 84 if (this._isCapturing) 85 return; 86 87 this._isCapturing = true; 88 89 this.dispatchEventToListeners(WebInspector.TimelineManager.Event.CapturingStarted); 90 }, 91 92 capturingStopped: function() 93 { 94 if (!this._isCapturing) 95 return; 96 97 if (this._stopCapturingTimeout) { 98 clearTimeout(this._stopCapturingTimeout); 99 delete this._stopCapturingTimeout; 100 } 101 102 if (this._deadTimeTimeout) { 103 clearTimeout(this._deadTimeTimeout); 104 delete this._deadTimeTimeout; 105 } 106 107 this._isCapturing = false; 108 this._autoCapturingMainResource = null; 109 110 this.dispatchEventToListeners(WebInspector.TimelineManager.Event.CapturingStopped); 111 }, 112 113 eventRecorded: function(originalRecordPayload) 114 { 115 // Called from WebInspector.TimelineObserver. 116 117 if (!this._isCapturing) 118 return; 119 120 function processRecord(recordPayload, parentRecordPayload) 121 { 122 // Convert the timestamps to seconds to match the resource timestamps. 123 var startTime = recordPayload.startTime / 1000; 124 var endTime = recordPayload.endTime / 1000; 125 126 var callFrames = this._callFramesFromPayload(recordPayload.stackTrace); 127 128 var significantCallFrame = null; 129 if (callFrames) { 130 for (var i = 0; i < callFrames.length; ++i) { 131 if (callFrames[i].nativeCode) 132 continue; 133 significantCallFrame = callFrames[i]; 134 break; 135 } 136 } 137 138 var sourceCodeLocation = significantCallFrame && significantCallFrame.sourceCodeLocation; 139 140 switch (recordPayload.type) { 141 case TimelineAgent.EventType.MarkLoad: 142 console.assert(isNaN(endTime)); 143 144 var frame = WebInspector.frameResourceManager.frameForIdentifier(recordPayload.frameId); 145 console.assert(frame); 146 if (!frame) 147 break; 148 149 frame.markLoadEvent(startTime); 150 151 if (!frame.isMainFrame()) 152 break; 153 154 var eventMarker = new WebInspector.TimelineMarker(startTime, WebInspector.TimelineMarker.Type.LoadEvent); 155 this._activeRecording.addEventMarker(eventMarker); 156 157 this._stopAutoRecordingSoon(); 158 break; 159 160 case TimelineAgent.EventType.MarkDOMContent: 161 console.assert(isNaN(endTime)); 162 163 var frame = WebInspector.frameResourceManager.frameForIdentifier(recordPayload.frameId); 164 console.assert(frame); 165 if (!frame) 166 break; 167 168 frame.markDOMContentReadyEvent(startTime); 169 170 if (!frame.isMainFrame()) 171 break; 172 173 var eventMarker = new WebInspector.TimelineMarker(startTime, WebInspector.TimelineMarker.Type.DOMContentEvent); 174 this._activeRecording.addEventMarker(eventMarker); 175 break; 176 177 case TimelineAgent.EventType.ScheduleStyleRecalculation: 178 console.assert(isNaN(endTime)); 179 180 // Pass the startTime as the endTime since this record type has no duration. 181 this._addRecord(new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.InvalidateStyles, startTime, startTime, callFrames, sourceCodeLocation)); 182 break; 183 184 case TimelineAgent.EventType.RecalculateStyles: 185 this._addRecord(new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.RecalculateStyles, startTime, endTime, callFrames, sourceCodeLocation)); 186 break; 187 188 case TimelineAgent.EventType.InvalidateLayout: 189 console.assert(isNaN(endTime)); 190 191 // Pass the startTime as the endTime since this record type has no duration. 192 this._addRecord(new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.InvalidateLayout, startTime, startTime, callFrames, sourceCodeLocation)); 193 break; 194 195 case TimelineAgent.EventType.Layout: 196 var layoutRecordType = sourceCodeLocation ? WebInspector.LayoutTimelineRecord.EventType.ForcedLayout : WebInspector.LayoutTimelineRecord.EventType.Layout; 197 198 // COMPATIBILITY (iOS 6): Layout records did not contain area properties. This is not exposed via a quad "root". 199 var quad = recordPayload.data.root ? new WebInspector.Quad(recordPayload.data.root) : null; 200 if (quad) 201 this._addRecord(new WebInspector.LayoutTimelineRecord(layoutRecordType, startTime, endTime, callFrames, sourceCodeLocation, quad.points[0].x, quad.points[0].y, quad.width, quad.height, quad)); 202 else 203 this._addRecord(new WebInspector.LayoutTimelineRecord(layoutRecordType, startTime, endTime, callFrames, sourceCodeLocation)); 204 break; 205 206 case TimelineAgent.EventType.Paint: 207 // COMPATIBILITY (iOS 6): Paint records data contained x, y, width, height properties. This became a quad "clip". 208 var quad = recordPayload.data.clip ? new WebInspector.Quad(recordPayload.data.clip) : null; 209 if (quad) 210 this._addRecord(new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.Paint, startTime, endTime, callFrames, sourceCodeLocation, null, null, quad.width, quad.height, quad)); 211 else 212 this._addRecord(new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.Paint, startTime, endTime, callFrames, sourceCodeLocation, recordPayload.data.x, recordPayload.data.y, recordPayload.data.width, recordPayload.data.height)); 213 break; 214 215 case TimelineAgent.EventType.EvaluateScript: 216 if (!sourceCodeLocation) { 217 var mainFrame = WebInspector.frameResourceManager.mainFrame; 218 var scriptResource = mainFrame.url === recordPayload.data.url ? mainFrame.mainResource : mainFrame.resourceForURL(recordPayload.data.url, true); 219 if (scriptResource) { 220 // The lineNumber is 1-based, but we expect 0-based. 221 var lineNumber = recordPayload.data.lineNumber - 1; 222 223 // FIXME: No column number is provided. 224 sourceCodeLocation = scriptResource.createSourceCodeLocation(lineNumber, 0); 225 } 226 } 227 228 var profileData = recordPayload.data.profile; 229 230 switch (parentRecordPayload && parentRecordPayload.type) { 231 case TimelineAgent.EventType.TimerFire: 232 this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.TimerFired, startTime, endTime, callFrames, sourceCodeLocation, parentRecordPayload.data.timerId, profileData)); 233 break; 234 default: 235 this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.ScriptEvaluated, startTime, endTime, callFrames, sourceCodeLocation, null, profileData)); 236 break; 237 } 238 239 break; 240 241 case TimelineAgent.EventType.TimeStamp: 242 var eventMarker = new WebInspector.TimelineMarker(startTime, WebInspector.TimelineMarker.Type.TimeStamp); 243 this._activeRecording.addEventMarker(eventMarker); 244 break; 245 246 case TimelineAgent.EventType.ConsoleProfile: 247 var profileData = recordPayload.data.profile; 248 console.assert(profileData); 249 this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.ConsoleProfileRecorded, startTime, endTime, callFrames, sourceCodeLocation, recordPayload.data.title, profileData)); 250 break; 251 252 case TimelineAgent.EventType.FunctionCall: 253 // FunctionCall always happens as a child of another record, and since the FunctionCall record 254 // has useful info we just make the timeline record here (combining the data from both records). 255 if (!parentRecordPayload) 256 break; 257 258 if (!sourceCodeLocation) { 259 var mainFrame = WebInspector.frameResourceManager.mainFrame; 260 var scriptResource = mainFrame.url === recordPayload.data.scriptName ? mainFrame.mainResource : mainFrame.resourceForURL(recordPayload.data.scriptName, true); 261 if (scriptResource) { 262 // The lineNumber is 1-based, but we expect 0-based. 263 var lineNumber = recordPayload.data.scriptLine - 1; 264 265 // FIXME: No column number is provided. 266 sourceCodeLocation = scriptResource.createSourceCodeLocation(lineNumber, 0); 267 } 268 } 269 270 var profileData = recordPayload.data.profile; 271 272 switch (parentRecordPayload.type) { 273 case TimelineAgent.EventType.TimerFire: 274 this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.TimerFired, startTime, endTime, callFrames, sourceCodeLocation, parentRecordPayload.data.timerId, profileData)); 275 break; 276 case TimelineAgent.EventType.EventDispatch: 277 this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.EventDispatched, startTime, endTime, callFrames, sourceCodeLocation, parentRecordPayload.data.type, profileData)); 278 break; 279 case TimelineAgent.EventType.XHRLoad: 280 this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.EventDispatched, startTime, endTime, callFrames, sourceCodeLocation, "load", profileData)); 281 break; 282 case TimelineAgent.EventType.XHRReadyStateChange: 283 this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.EventDispatched, startTime, endTime, callFrames, sourceCodeLocation, "readystatechange", profileData)); 284 break; 285 case TimelineAgent.EventType.FireAnimationFrame: 286 this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.AnimationFrameFired, startTime, endTime, callFrames, sourceCodeLocation, parentRecordPayload.data.id, profileData)); 287 break; 288 } 289 290 break; 291 292 case TimelineAgent.EventType.ProbeSample: 293 // Pass the startTime as the endTime since this record type has no duration. 294 sourceCodeLocation = WebInspector.probeManager.probeForIdentifier(recordPayload.data.probeId).breakpoint.sourceCodeLocation; 295 this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.ProbeSampleRecorded, startTime, startTime, callFrames, sourceCodeLocation, recordPayload.data.probeId)); 296 break; 297 298 case TimelineAgent.EventType.TimerInstall: 299 console.assert(isNaN(endTime)); 300 301 // Pass the startTime as the endTime since this record type has no duration. 302 this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.TimerInstalled, startTime, startTime, callFrames, sourceCodeLocation, recordPayload.data.timerId)); 303 break; 304 305 case TimelineAgent.EventType.TimerRemove: 306 console.assert(isNaN(endTime)); 307 308 // Pass the startTime as the endTime since this record type has no duration. 309 this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.TimerRemoved, startTime, startTime, callFrames, sourceCodeLocation, recordPayload.data.timerId)); 310 break; 311 312 case TimelineAgent.EventType.RequestAnimationFrame: 313 console.assert(isNaN(endTime)); 314 315 // Pass the startTime as the endTime since this record type has no duration. 316 this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.AnimationFrameRequested, startTime, startTime, callFrames, sourceCodeLocation, recordPayload.data.timerId)); 317 break; 318 319 case TimelineAgent.EventType.CancelAnimationFrame: 320 console.assert(isNaN(endTime)); 321 322 // Pass the startTime as the endTime since this record type has no duration. 323 this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.AnimationFrameCanceled, startTime, startTime, callFrames, sourceCodeLocation, recordPayload.data.timerId)); 324 break; 325 } 326 } 327 328 // Iterate over the records tree using a stack. Doing this recursively has 329 // been known to cause a call stack overflow. https://webkit.org/b/79106 330 var stack = [{array: [originalRecordPayload], parent: null, index: 0}]; 331 while (stack.length) { 332 var entry = stack.lastValue; 333 var recordPayloads = entry.array; 334 var parentRecordPayload = entry.parent; 335 336 if (entry.index < recordPayloads.length) { 337 var recordPayload = recordPayloads[entry.index]; 338 339 processRecord.call(this, recordPayload, parentRecordPayload); 340 341 if (recordPayload.children) 342 stack.push({array: recordPayload.children, parent: recordPayload, index: 0}); 343 ++entry.index; 344 } else 345 stack.pop(); 346 } 347 }, 348 349 pageDidLoad: function(timestamp) 350 { 351 if (isNaN(WebInspector.frameResourceManager.mainFrame.loadEventTimestamp)) 352 WebInspector.frameResourceManager.mainFrame.markLoadEvent(timestamp); 353 }, 354 355 // Private 356 357 _callFramesFromPayload: function(payload) 358 { 359 if (!payload) 360 return null; 361 362 function createCallFrame(payload) 363 { 364 var url = payload.url; 365 var nativeCode = false; 366 367 if (url === "[native code]") { 368 nativeCode = true; 369 url = null; 370 } 371 372 var sourceCode = WebInspector.frameResourceManager.resourceForURL(url); 373 if (!sourceCode) 374 sourceCode = WebInspector.debuggerManager.scriptsForURL(url)[0]; 375 376 // The lineNumber is 1-based, but we expect 0-based. 377 var lineNumber = payload.lineNumber - 1; 378 379 var sourceCodeLocation = sourceCode ? sourceCode.createLazySourceCodeLocation(lineNumber, payload.columnNumber) : null; 380 var functionName = payload.functionName !== "global code" ? payload.functionName : null; 381 382 return new WebInspector.CallFrame(null, sourceCodeLocation, functionName, null, null, nativeCode); 383 } 384 385 return payload.map(createCallFrame); 386 }, 387 388 _addRecord: function(record) 389 { 390 this._activeRecording.addRecord(record); 391 392 // Only worry about dead time after the load event. 393 if (!isNaN(WebInspector.frameResourceManager.mainFrame.loadEventTimestamp)) 394 this._resetAutoRecordingDeadTimeTimeout(); 395 }, 396 397 _startAutoCapturing: function(event) 398 { 399 if (!event.target.isMainFrame() || (this._isCapturing && !this._autoCapturingMainResource)) 400 return false; 401 402 var mainResource = event.target.provisionalMainResource || event.target.mainResource; 403 if (mainResource === this._autoCapturingMainResource) 404 return false; 405 406 this.stopCapturing(); 407 408 this._autoCapturingMainResource = mainResource; 409 410 this._activeRecording.reset(); 411 412 this.startCapturing(); 413 414 this._addRecord(new WebInspector.ResourceTimelineRecord(mainResource)); 415 416 if (this._stopCapturingTimeout) 417 clearTimeout(this._stopCapturingTimeout); 418 this._stopCapturingTimeout = setTimeout(this._boundStopCapturing, WebInspector.TimelineManager.MaximumAutoRecordDuration); 419 420 return true; 421 }, 422 423 _stopAutoRecordingSoon: function() 424 { 425 // Only auto stop when auto capturing. 426 if (!this._isCapturing || !this._autoCapturingMainResource) 427 return; 428 429 if (this._stopCapturingTimeout) 430 clearTimeout(this._stopCapturingTimeout); 431 this._stopCapturingTimeout = setTimeout(this._boundStopCapturing, WebInspector.TimelineManager.MaximumAutoRecordDurationAfterLoadEvent); 432 }, 433 434 _resetAutoRecordingDeadTimeTimeout: function() 435 { 436 // Only monitor dead time when auto capturing. 437 if (!this._isCapturing || !this._autoCapturingMainResource) 438 return; 439 440 if (this._deadTimeTimeout) 441 clearTimeout(this._deadTimeTimeout); 442 this._deadTimeTimeout = setTimeout(this._boundStopCapturing, WebInspector.TimelineManager.DeadTimeRequiredToStopAutoRecordingEarly); 443 }, 444 445 _mainResourceDidChange: function(event) 446 { 447 // Ignore resource events when there isn't a main frame yet. Those events are triggered by 448 // loading the cached resources when the inspector opens, and they do not have timing information. 449 if (!WebInspector.frameResourceManager.mainFrame) 450 return; 451 452 if (this._startAutoCapturing(event)) 453 return; 454 455 if (!this._isCapturing) 456 return; 457 458 var mainResource = event.target.mainResource; 459 if (mainResource === this._autoCapturingMainResource) 460 return; 461 462 this._addRecord(new WebInspector.ResourceTimelineRecord(mainResource)); 463 }, 464 465 _resourceWasAdded: function(event) 466 { 467 // Ignore resource events when there isn't a main frame yet. Those events are triggered by 468 // loading the cached resources when the inspector opens, and they do not have timing information. 469 if (!WebInspector.frameResourceManager.mainFrame) 470 return; 471 472 if (!this._isCapturing) 473 return; 474 475 this._addRecord(new WebInspector.ResourceTimelineRecord(event.data.resource)); 476 } 477}; 478 479WebInspector.TimelineManager.prototype.__proto__ = WebInspector.Object.prototype; 480