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