1 // Copyright 2014 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 /** 6 * @constructor 7 * @extends {WebInspector.TimelineModel} 8 * @implements {WebInspector.TargetManager.Observer} 9 */ 10 WebInspector.TimelineModelImpl = function() 11 { 12 WebInspector.TimelineModel.call(this); 13 /** @type {?WebInspector.Target} */ 14 this._currentTarget = null; 15 this._filters = []; 16 this._bindings = new WebInspector.TimelineModelImpl.InterRecordBindings(); 17 18 this.reset(); 19 20 WebInspector.targetManager.addModelListener(WebInspector.TimelineManager, WebInspector.TimelineManager.EventTypes.TimelineEventRecorded, this._onRecordAdded, this); 21 WebInspector.targetManager.addModelListener(WebInspector.TimelineManager, WebInspector.TimelineManager.EventTypes.TimelineStarted, this._onStarted, this); 22 WebInspector.targetManager.addModelListener(WebInspector.TimelineManager, WebInspector.TimelineManager.EventTypes.TimelineStopped, this._onStopped, this); 23 WebInspector.targetManager.addModelListener(WebInspector.TimelineManager, WebInspector.TimelineManager.EventTypes.TimelineProgress, this._onProgress, this); 24 WebInspector.targetManager.observeTargets(this); 25 } 26 27 WebInspector.TimelineModelImpl.TransferChunkLengthBytes = 5000000; 28 29 WebInspector.TimelineModelImpl.prototype = { 30 /** 31 * @param {!WebInspector.Target} target 32 */ 33 targetAdded: function(target) { }, 34 35 /** 36 * @param {!WebInspector.Target} target 37 */ 38 targetRemoved: function(target) 39 { 40 if (this._currentTarget === target) 41 this._currentTarget = null; 42 }, 43 44 /** 45 * @param {boolean} captureStacks 46 * @param {boolean} captureMemory 47 * @param {boolean} capturePictures 48 */ 49 startRecording: function(captureStacks, captureMemory, capturePictures) 50 { 51 console.assert(!capturePictures, "Legacy timeline does not support capturing pictures"); 52 this.reset(); 53 this._currentTarget = WebInspector.context.flavor(WebInspector.Target); 54 console.assert(this._currentTarget); 55 56 this._clientInitiatedRecording = true; 57 var maxStackFrames = captureStacks ? 30 : 0; 58 var includeGPUEvents = Runtime.experiments.isEnabled("gpuTimeline"); 59 var liveEvents = [ WebInspector.TimelineModel.RecordType.BeginFrame, 60 WebInspector.TimelineModel.RecordType.DrawFrame, 61 WebInspector.TimelineModel.RecordType.RequestMainThreadFrame, 62 WebInspector.TimelineModel.RecordType.ActivateLayerTree ]; 63 this._currentTarget.timelineManager.start(maxStackFrames, liveEvents.join(","), captureMemory, includeGPUEvents, this._fireRecordingStarted.bind(this)); 64 }, 65 66 stopRecording: function() 67 { 68 if (!this._currentTarget) 69 return; 70 71 if (!this._clientInitiatedRecording) { 72 this._currentTarget.timelineManager.start(undefined, undefined, undefined, undefined, stopTimeline.bind(this)); 73 return; 74 } 75 76 /** 77 * Console started this one and we are just sniffing it. Initiate recording so that we 78 * could stop it. 79 * @this {WebInspector.TimelineModelImpl} 80 */ 81 function stopTimeline() 82 { 83 this._currentTarget.timelineManager.stop(this._fireRecordingStopped.bind(this)); 84 } 85 86 this._clientInitiatedRecording = false; 87 this._currentTarget.timelineManager.stop(this._fireRecordingStopped.bind(this)); 88 }, 89 90 /** 91 * @return {!Array.<!WebInspector.TimelineModel.Record>} 92 */ 93 records: function() 94 { 95 return this._records; 96 }, 97 98 /** 99 * @param {!WebInspector.Event} event 100 */ 101 _onRecordAdded: function(event) 102 { 103 var timelineManager = /** @type {!WebInspector.TimelineManager} */ (event.target); 104 if (this._collectionEnabled && timelineManager.target() === this._currentTarget) 105 this._addRecord(/** @type {!TimelineAgent.TimelineEvent} */(event.data)); 106 }, 107 108 /** 109 * @param {!WebInspector.Event} event 110 */ 111 _onStarted: function(event) 112 { 113 if (!event.data || this._collectionEnabled) 114 return; 115 // Started from console. 116 var timelineManager = /** @type {!WebInspector.TimelineManager} */ (event.target); 117 if (this._currentTarget !== timelineManager.target()) { 118 this.reset(); 119 this._currentTarget = timelineManager.target(); 120 } 121 this._fireRecordingStarted(); 122 }, 123 124 /** 125 * @param {!WebInspector.Event} event 126 */ 127 _onStopped: function(event) 128 { 129 var timelineManager = /** @type {!WebInspector.TimelineManager} */ (event.target); 130 if (timelineManager.target() !== this._currentTarget) 131 return; 132 // We were buffering events, discard those that got through, the real ones are coming! 133 this.reset(); 134 this._currentTarget = timelineManager.target(); 135 136 var events = /** @type {!Array.<!TimelineAgent.TimelineEvent>} */ (event.data.events); 137 for (var i = 0; i < events.length; ++i) 138 this._addRecord(events[i]); 139 140 if (event.data.consoleTimeline) { 141 // Stopped from console. 142 this._fireRecordingStopped(null, null); 143 } 144 145 this._collectionEnabled = false; 146 }, 147 148 /** 149 * @param {!WebInspector.Event} event 150 */ 151 _onProgress: function(event) 152 { 153 var timelineManager = /** @type {!WebInspector.TimelineManager} */ (event.target); 154 if (timelineManager.target() === this._currentTarget) 155 this.dispatchEventToListeners(WebInspector.TimelineModel.Events.RecordingProgress, event.data); 156 }, 157 158 _fireRecordingStarted: function() 159 { 160 this._collectionEnabled = true; 161 this.dispatchEventToListeners(WebInspector.TimelineModel.Events.RecordingStarted); 162 }, 163 164 /** 165 * @param {?Protocol.Error} error 166 * @param {?ProfilerAgent.CPUProfile} cpuProfile 167 */ 168 _fireRecordingStopped: function(error, cpuProfile) 169 { 170 if (cpuProfile) 171 WebInspector.TimelineJSProfileProcessor.mergeJSProfileIntoTimeline(this, cpuProfile); 172 this.dispatchEventToListeners(WebInspector.TimelineModel.Events.RecordingStopped); 173 }, 174 175 /** 176 * @param {!TimelineAgent.TimelineEvent} payload 177 */ 178 _addRecord: function(payload) 179 { 180 this._internStrings(payload); 181 this._payloads.push(payload); 182 183 var record = this._innerAddRecord(payload, null); 184 this._updateBoundaries(record); 185 this._records.push(record); 186 if (record.type() === WebInspector.TimelineModel.RecordType.Program) 187 this._mainThreadTasks.push(record); 188 if (record.type() === WebInspector.TimelineModel.RecordType.GPUTask) 189 this._gpuThreadTasks.push(record); 190 191 this.dispatchEventToListeners(WebInspector.TimelineModel.Events.RecordAdded, record); 192 }, 193 194 /** 195 * @param {!TimelineAgent.TimelineEvent} payload 196 * @param {?WebInspector.TimelineModel.Record} parentRecord 197 * @return {!WebInspector.TimelineModel.Record} 198 */ 199 _innerAddRecord: function(payload, parentRecord) 200 { 201 var record = new WebInspector.TimelineModel.RecordImpl(this, payload, parentRecord); 202 if (WebInspector.TimelineUIUtilsImpl.isEventDivider(record)) 203 this._eventDividerRecords.push(record); 204 205 for (var i = 0; payload.children && i < payload.children.length; ++i) 206 this._innerAddRecord.call(this, payload.children[i], record); 207 208 if (parentRecord) 209 parentRecord._selfTime -= record.endTime() - record.startTime(); 210 return record; 211 }, 212 213 /** 214 * @param {!WebInspector.ChunkedFileReader} fileReader 215 * @param {!WebInspector.Progress} progress 216 * @return {!WebInspector.OutputStream} 217 */ 218 createLoader: function(fileReader, progress) 219 { 220 return new WebInspector.TimelineModelLoader(this, fileReader, progress); 221 }, 222 223 /** 224 * @param {!WebInspector.OutputStream} stream 225 */ 226 writeToStream: function(stream) 227 { 228 var saver = new WebInspector.TimelineSaver(stream); 229 saver.save(this._payloads, window.navigator.appVersion); 230 }, 231 232 reset: function() 233 { 234 if (!this._collectionEnabled) 235 this._currentTarget = null; 236 this._payloads = []; 237 this._stringPool = {}; 238 this._bindings._reset(); 239 WebInspector.TimelineModel.prototype.reset.call(this); 240 }, 241 242 /** 243 * @param {!TimelineAgent.TimelineEvent} record 244 */ 245 _internStrings: function(record) 246 { 247 for (var name in record) { 248 var value = record[name]; 249 if (typeof value !== "string") 250 continue; 251 252 var interned = this._stringPool[value]; 253 if (typeof interned === "string") 254 record[name] = interned; 255 else 256 this._stringPool[value] = value; 257 } 258 259 var children = record.children; 260 for (var i = 0; children && i < children.length; ++i) 261 this._internStrings(children[i]); 262 }, 263 264 __proto__: WebInspector.TimelineModel.prototype 265 } 266 267 268 /** 269 * @constructor 270 */ 271 WebInspector.TimelineModelImpl.InterRecordBindings = function() { 272 this._reset(); 273 } 274 275 WebInspector.TimelineModelImpl.InterRecordBindings.prototype = { 276 _reset: function() 277 { 278 this._sendRequestRecords = {}; 279 this._timerRecords = {}; 280 this._requestAnimationFrameRecords = {}; 281 this._layoutInvalidate = {}; 282 this._lastScheduleStyleRecalculation = {}; 283 this._webSocketCreateRecords = {}; 284 } 285 } 286 287 /** 288 * @constructor 289 * @implements {WebInspector.TimelineModel.Record} 290 * @param {!WebInspector.TimelineModel} model 291 * @param {!TimelineAgent.TimelineEvent} timelineEvent 292 * @param {?WebInspector.TimelineModel.Record} parentRecord 293 */ 294 WebInspector.TimelineModel.RecordImpl = function(model, timelineEvent, parentRecord) 295 { 296 this._model = model; 297 var bindings = this._model._bindings; 298 this._record = timelineEvent; 299 this._thread = this._record.thread || WebInspector.TimelineModel.MainThreadName; 300 this._children = []; 301 if (parentRecord) { 302 this.parent = parentRecord; 303 parentRecord.children().push(this); 304 } 305 306 this._selfTime = this.endTime() - this.startTime(); 307 308 var recordTypes = WebInspector.TimelineModel.RecordType; 309 switch (timelineEvent.type) { 310 case recordTypes.ResourceSendRequest: 311 // Make resource receive record last since request was sent; make finish record last since response received. 312 bindings._sendRequestRecords[timelineEvent.data["requestId"]] = this; 313 break; 314 315 case recordTypes.ResourceReceiveResponse: 316 case recordTypes.ResourceReceivedData: 317 case recordTypes.ResourceFinish: 318 this._initiator = bindings._sendRequestRecords[timelineEvent.data["requestId"]]; 319 break; 320 321 case recordTypes.TimerInstall: 322 bindings._timerRecords[timelineEvent.data["timerId"]] = this; 323 break; 324 325 case recordTypes.TimerFire: 326 this._initiator = bindings._timerRecords[timelineEvent.data["timerId"]]; 327 break; 328 329 case recordTypes.RequestAnimationFrame: 330 bindings._requestAnimationFrameRecords[timelineEvent.data["id"]] = this; 331 break; 332 333 case recordTypes.FireAnimationFrame: 334 this._initiator = bindings._requestAnimationFrameRecords[timelineEvent.data["id"]]; 335 break; 336 337 case recordTypes.ScheduleStyleRecalculation: 338 bindings._lastScheduleStyleRecalculation[this.frameId()] = this; 339 break; 340 341 case recordTypes.RecalculateStyles: 342 this._initiator = bindings._lastScheduleStyleRecalculation[this.frameId()]; 343 break; 344 345 case recordTypes.InvalidateLayout: 346 // Consider style recalculation as a reason for layout invalidation, 347 // but only if we had no earlier layout invalidation records. 348 var layoutInitator = this; 349 if (!bindings._layoutInvalidate[this.frameId()] && parentRecord.type() === recordTypes.RecalculateStyles) 350 layoutInitator = parentRecord._initiator; 351 bindings._layoutInvalidate[this.frameId()] = layoutInitator; 352 break; 353 354 case recordTypes.Layout: 355 this._initiator = bindings._layoutInvalidate[this.frameId()]; 356 bindings._layoutInvalidate[this.frameId()] = null; 357 if (this.stackTrace()) 358 this.addWarning(WebInspector.UIString("Forced synchronous layout is a possible performance bottleneck.")); 359 break; 360 361 case recordTypes.WebSocketCreate: 362 bindings._webSocketCreateRecords[timelineEvent.data["identifier"]] = this; 363 break; 364 365 case recordTypes.WebSocketSendHandshakeRequest: 366 case recordTypes.WebSocketReceiveHandshakeResponse: 367 case recordTypes.WebSocketDestroy: 368 this._initiator = bindings._webSocketCreateRecords[timelineEvent.data["identifier"]]; 369 break; 370 } 371 } 372 373 WebInspector.TimelineModel.RecordImpl.prototype = { 374 /** 375 * @return {?Array.<!ConsoleAgent.CallFrame>} 376 */ 377 callSiteStackTrace: function() 378 { 379 return this._initiator ? this._initiator.stackTrace() : null; 380 }, 381 382 /** 383 * @return {?WebInspector.TimelineModel.Record} 384 */ 385 initiator: function() 386 { 387 return this._initiator; 388 }, 389 390 /** 391 * @return {?WebInspector.Target} 392 */ 393 target: function() 394 { 395 return this._model._currentTarget; 396 }, 397 398 /** 399 * @return {number} 400 */ 401 selfTime: function() 402 { 403 return this._selfTime; 404 }, 405 406 /** 407 * @return {!Array.<!WebInspector.TimelineModel.Record>} 408 */ 409 children: function() 410 { 411 return this._children; 412 }, 413 414 /** 415 * @return {number} 416 */ 417 startTime: function() 418 { 419 return this._record.startTime; 420 }, 421 422 /** 423 * @return {string} 424 */ 425 thread: function() 426 { 427 return this._thread; 428 }, 429 430 /** 431 * @return {number} 432 */ 433 endTime: function() 434 { 435 return this._endTime || this._record.endTime || this._record.startTime; 436 }, 437 438 /** 439 * @param {number} endTime 440 */ 441 setEndTime: function(endTime) 442 { 443 this._endTime = endTime; 444 }, 445 446 /** 447 * @return {!Object} 448 */ 449 data: function() 450 { 451 return this._record.data; 452 }, 453 454 /** 455 * @return {string} 456 */ 457 type: function() 458 { 459 return this._record.type; 460 }, 461 462 /** 463 * @return {string} 464 */ 465 frameId: function() 466 { 467 return this._record.frameId || ""; 468 }, 469 470 /** 471 * @return {?Array.<!ConsoleAgent.CallFrame>} 472 */ 473 stackTrace: function() 474 { 475 if (this._record.stackTrace && this._record.stackTrace.length) 476 return this._record.stackTrace; 477 return null; 478 }, 479 480 /** 481 * @param {string} key 482 * @return {?Object} 483 */ 484 getUserObject: function(key) 485 { 486 if (!this._userObjects) 487 return null; 488 return this._userObjects.get(key); 489 }, 490 491 /** 492 * @param {string} key 493 * @param {?Object|undefined} value 494 */ 495 setUserObject: function(key, value) 496 { 497 if (!this._userObjects) 498 this._userObjects = new StringMap(); 499 this._userObjects.set(key, value); 500 }, 501 502 /** 503 * @param {string} message 504 */ 505 addWarning: function(message) 506 { 507 if (!this._warnings) 508 this._warnings = []; 509 this._warnings.push(message); 510 }, 511 512 /** 513 * @return {?Array.<string>} 514 */ 515 warnings: function() 516 { 517 return this._warnings; 518 } 519 } 520 521 /** 522 * @constructor 523 * @implements {WebInspector.OutputStream} 524 * @param {!WebInspector.TimelineModel} model 525 * @param {!{cancel: function()}} reader 526 * @param {!WebInspector.Progress} progress 527 */ 528 WebInspector.TimelineModelLoader = function(model, reader, progress) 529 { 530 this._model = model; 531 this._reader = reader; 532 this._progress = progress; 533 this._buffer = ""; 534 this._firstChunk = true; 535 } 536 537 WebInspector.TimelineModelLoader.prototype = { 538 /** 539 * @param {string} chunk 540 */ 541 write: function(chunk) 542 { 543 var data = this._buffer + chunk; 544 var lastIndex = 0; 545 var index; 546 do { 547 index = lastIndex; 548 lastIndex = WebInspector.TextUtils.findBalancedCurlyBrackets(data, index); 549 } while (lastIndex !== -1) 550 551 var json = data.slice(0, index) + "]"; 552 this._buffer = data.slice(index); 553 554 if (!index) 555 return; 556 557 if (this._firstChunk) { 558 this._firstChunk = false; 559 this._model.reset(); 560 } else { 561 // Prepending "0" to turn string into valid JSON. 562 json = "[0" + json; 563 } 564 565 var items; 566 try { 567 items = /** @type {!Array.<!TimelineAgent.TimelineEvent>} */ (JSON.parse(json)); 568 } catch (e) { 569 WebInspector.console.error("Malformed timeline data."); 570 this._model.reset(); 571 this._reader.cancel(); 572 this._progress.done(); 573 return; 574 } 575 576 // Skip 0-th element - it is either version or 0. 577 for (var i = 1, size = items.length; i < size; ++i) 578 this._model._addRecord(items[i]); 579 }, 580 581 close: function() 582 { 583 } 584 } 585 586 /** 587 * @constructor 588 * @param {!WebInspector.OutputStream} stream 589 */ 590 WebInspector.TimelineSaver = function(stream) 591 { 592 this._stream = stream; 593 } 594 595 WebInspector.TimelineSaver.prototype = { 596 /** 597 * @param {!Array.<*>} payloads 598 * @param {string} version 599 */ 600 save: function(payloads, version) 601 { 602 this._payloads = payloads; 603 this._recordIndex = 0; 604 this._prologue = "[" + JSON.stringify(version); 605 606 this._writeNextChunk(this._stream); 607 }, 608 609 _writeNextChunk: function(stream) 610 { 611 const separator = ",\n"; 612 var data = []; 613 var length = 0; 614 615 if (this._prologue) { 616 data.push(this._prologue); 617 length += this._prologue.length; 618 delete this._prologue; 619 } else { 620 if (this._recordIndex === this._payloads.length) { 621 stream.close(); 622 return; 623 } 624 data.push(""); 625 } 626 while (this._recordIndex < this._payloads.length) { 627 var item = JSON.stringify(this._payloads[this._recordIndex]); 628 var itemLength = item.length + separator.length; 629 if (length + itemLength > WebInspector.TimelineModelImpl.TransferChunkLengthBytes) 630 break; 631 length += itemLength; 632 data.push(item); 633 ++this._recordIndex; 634 } 635 if (this._recordIndex === this._payloads.length) 636 data.push(data.pop() + "]"); 637 stream.write(data.join(separator), this._writeNextChunk.bind(this)); 638 } 639 } 640