1/* 2 * Copyright (C) 2012 Google 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 are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google Inc. nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31/** 32 * @constructor 33 * @extends {WebInspector.Object} 34 */ 35WebInspector.TimelineModel = function() 36{ 37 this._records = []; 38 this._stringPool = new StringPool(); 39 this._minimumRecordTime = -1; 40 this._maximumRecordTime = -1; 41 42 WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.EventTypes.TimelineEventRecorded, this._onRecordAdded, this); 43 WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.EventTypes.TimelineStarted, this._onStarted, this); 44 WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.EventTypes.TimelineStopped, this._onStopped, this); 45} 46 47WebInspector.TimelineModel.TransferChunkLengthBytes = 5000000; 48 49WebInspector.TimelineModel.RecordType = { 50 Root: "Root", 51 Program: "Program", 52 EventDispatch: "EventDispatch", 53 54 GPUTask: "GPUTask", 55 56 BeginFrame: "BeginFrame", 57 ActivateLayerTree: "ActivateLayerTree", 58 ScheduleStyleRecalculation: "ScheduleStyleRecalculation", 59 RecalculateStyles: "RecalculateStyles", 60 InvalidateLayout: "InvalidateLayout", 61 Layout: "Layout", 62 AutosizeText: "AutosizeText", 63 PaintSetup: "PaintSetup", 64 Paint: "Paint", 65 Rasterize: "Rasterize", 66 ScrollLayer: "ScrollLayer", 67 DecodeImage: "DecodeImage", 68 ResizeImage: "ResizeImage", 69 CompositeLayers: "CompositeLayers", 70 71 ParseHTML: "ParseHTML", 72 73 TimerInstall: "TimerInstall", 74 TimerRemove: "TimerRemove", 75 TimerFire: "TimerFire", 76 77 XHRReadyStateChange: "XHRReadyStateChange", 78 XHRLoad: "XHRLoad", 79 EvaluateScript: "EvaluateScript", 80 81 MarkLoad: "MarkLoad", 82 MarkDOMContent: "MarkDOMContent", 83 MarkFirstPaint: "MarkFirstPaint", 84 85 TimeStamp: "TimeStamp", 86 Time: "Time", 87 TimeEnd: "TimeEnd", 88 89 ScheduleResourceRequest: "ScheduleResourceRequest", 90 ResourceSendRequest: "ResourceSendRequest", 91 ResourceReceiveResponse: "ResourceReceiveResponse", 92 ResourceReceivedData: "ResourceReceivedData", 93 ResourceFinish: "ResourceFinish", 94 95 FunctionCall: "FunctionCall", 96 GCEvent: "GCEvent", 97 98 RequestAnimationFrame: "RequestAnimationFrame", 99 CancelAnimationFrame: "CancelAnimationFrame", 100 FireAnimationFrame: "FireAnimationFrame", 101 102 WebSocketCreate : "WebSocketCreate", 103 WebSocketSendHandshakeRequest : "WebSocketSendHandshakeRequest", 104 WebSocketReceiveHandshakeResponse : "WebSocketReceiveHandshakeResponse", 105 WebSocketDestroy : "WebSocketDestroy", 106} 107 108WebInspector.TimelineModel.Events = { 109 RecordAdded: "RecordAdded", 110 RecordsCleared: "RecordsCleared", 111 RecordingStarted: "RecordingStarted", 112 RecordingStopped: "RecordingStopped" 113} 114 115WebInspector.TimelineModel.startTimeInSeconds = function(record) 116{ 117 return record.startTime / 1000; 118} 119 120WebInspector.TimelineModel.endTimeInSeconds = function(record) 121{ 122 return (record.endTime || record.startTime) / 1000; 123} 124 125WebInspector.TimelineModel.durationInSeconds = function(record) 126{ 127 return WebInspector.TimelineModel.endTimeInSeconds(record) - WebInspector.TimelineModel.startTimeInSeconds(record); 128} 129 130/** 131 * @param {!Object} total 132 * @param {!Object} rawRecord 133 */ 134WebInspector.TimelineModel.aggregateTimeForRecord = function(total, rawRecord) 135{ 136 var childrenTime = 0; 137 var children = rawRecord["children"] || []; 138 for (var i = 0; i < children.length; ++i) { 139 WebInspector.TimelineModel.aggregateTimeForRecord(total, children[i]); 140 childrenTime += WebInspector.TimelineModel.durationInSeconds(children[i]); 141 } 142 var categoryName = WebInspector.TimelinePresentationModel.recordStyle(rawRecord).category.name; 143 var ownTime = WebInspector.TimelineModel.durationInSeconds(rawRecord) - childrenTime; 144 total[categoryName] = (total[categoryName] || 0) + ownTime; 145} 146 147/** 148 * @param {!Object} total 149 * @param {!Object} addend 150 */ 151WebInspector.TimelineModel.aggregateTimeByCategory = function(total, addend) 152{ 153 for (var category in addend) 154 total[category] = (total[category] || 0) + addend[category]; 155} 156 157WebInspector.TimelineModel.prototype = { 158 /** 159 * @param {boolean=} includeDomCounters 160 */ 161 startRecording: function(includeDomCounters) 162 { 163 this._clientInitiatedRecording = true; 164 this.reset(); 165 var maxStackFrames = WebInspector.settings.timelineCaptureStacks.get() ? 30 : 0; 166 var includeGPUEvents = WebInspector.experimentsSettings.gpuTimeline.isEnabled(); 167 WebInspector.timelineManager.start(maxStackFrames, includeDomCounters, includeGPUEvents, this._fireRecordingStarted.bind(this)); 168 }, 169 170 stopRecording: function() 171 { 172 if (!this._clientInitiatedRecording) { 173 WebInspector.timelineManager.start(undefined, undefined, undefined, stopTimeline.bind(this)); 174 return; 175 } 176 177 /** 178 * Console started this one and we are just sniffing it. Initiate recording so that we 179 * could stop it. 180 * @this {WebInspector.TimelineModel} 181 */ 182 function stopTimeline() 183 { 184 WebInspector.timelineManager.stop(this._fireRecordingStopped.bind(this)); 185 } 186 187 this._clientInitiatedRecording = false; 188 WebInspector.timelineManager.stop(this._fireRecordingStopped.bind(this)); 189 }, 190 191 get records() 192 { 193 return this._records; 194 }, 195 196 /** 197 * @param {!WebInspector.Event} event 198 */ 199 _onRecordAdded: function(event) 200 { 201 if (this._collectionEnabled) 202 this._addRecord(/** @type {!TimelineAgent.TimelineEvent} */(event.data)); 203 }, 204 205 /** 206 * @param {!WebInspector.Event} event 207 */ 208 _onStarted: function(event) 209 { 210 if (event.data) { 211 // Started from console. 212 this._fireRecordingStarted(); 213 } 214 }, 215 216 /** 217 * @param {!WebInspector.Event} event 218 */ 219 _onStopped: function(event) 220 { 221 if (event.data) { 222 // Stopped from console. 223 this._fireRecordingStopped(); 224 } 225 }, 226 227 _fireRecordingStarted: function() 228 { 229 this._collectionEnabled = true; 230 this.dispatchEventToListeners(WebInspector.TimelineModel.Events.RecordingStarted); 231 }, 232 233 _fireRecordingStopped: function() 234 { 235 this._collectionEnabled = false; 236 this.dispatchEventToListeners(WebInspector.TimelineModel.Events.RecordingStopped); 237 }, 238 239 /** 240 * @param {!TimelineAgent.TimelineEvent} record 241 */ 242 _addRecord: function(record) 243 { 244 this._stringPool.internObjectStrings(record); 245 this._records.push(record); 246 this._updateBoundaries(record); 247 this.dispatchEventToListeners(WebInspector.TimelineModel.Events.RecordAdded, record); 248 }, 249 250 /** 251 * @param {!Blob} file 252 * @param {!WebInspector.Progress} progress 253 */ 254 loadFromFile: function(file, progress) 255 { 256 var delegate = new WebInspector.TimelineModelLoadFromFileDelegate(this, progress); 257 var fileReader = this._createFileReader(file, delegate); 258 var loader = new WebInspector.TimelineModelLoader(this, fileReader, progress); 259 fileReader.start(loader); 260 }, 261 262 /** 263 * @param {string} url 264 */ 265 loadFromURL: function(url, progress) 266 { 267 var delegate = new WebInspector.TimelineModelLoadFromFileDelegate(this, progress); 268 var urlReader = new WebInspector.ChunkedXHRReader(url, delegate); 269 var loader = new WebInspector.TimelineModelLoader(this, urlReader, progress); 270 urlReader.start(loader); 271 }, 272 273 _createFileReader: function(file, delegate) 274 { 275 return new WebInspector.ChunkedFileReader(file, WebInspector.TimelineModel.TransferChunkLengthBytes, delegate); 276 }, 277 278 _createFileWriter: function() 279 { 280 return new WebInspector.FileOutputStream(); 281 }, 282 283 saveToFile: function() 284 { 285 var now = new Date(); 286 var fileName = "TimelineRawData-" + now.toISO8601Compact() + ".json"; 287 var stream = this._createFileWriter(); 288 289 /** 290 * @param {boolean} accepted 291 * @this {WebInspector.TimelineModel} 292 */ 293 function callback(accepted) 294 { 295 if (!accepted) 296 return; 297 var saver = new WebInspector.TimelineSaver(stream); 298 saver.save(this._records, window.navigator.appVersion); 299 } 300 stream.open(fileName, callback.bind(this)); 301 }, 302 303 reset: function() 304 { 305 this._records = []; 306 this._stringPool.reset(); 307 this._minimumRecordTime = -1; 308 this._maximumRecordTime = -1; 309 this.dispatchEventToListeners(WebInspector.TimelineModel.Events.RecordsCleared); 310 }, 311 312 minimumRecordTime: function() 313 { 314 return this._minimumRecordTime; 315 }, 316 317 maximumRecordTime: function() 318 { 319 return this._maximumRecordTime; 320 }, 321 322 /** 323 * @param {!TimelineAgent.TimelineEvent} record 324 */ 325 _updateBoundaries: function(record) 326 { 327 var startTime = WebInspector.TimelineModel.startTimeInSeconds(record); 328 var endTime = WebInspector.TimelineModel.endTimeInSeconds(record); 329 330 if (this._minimumRecordTime === -1 || startTime < this._minimumRecordTime) 331 this._minimumRecordTime = startTime; 332 if (this._maximumRecordTime === -1 || endTime > this._maximumRecordTime) 333 this._maximumRecordTime = endTime; 334 }, 335 336 /** 337 * @param {!Object} rawRecord 338 */ 339 recordOffsetInSeconds: function(rawRecord) 340 { 341 return WebInspector.TimelineModel.startTimeInSeconds(rawRecord) - this._minimumRecordTime; 342 }, 343 344 __proto__: WebInspector.Object.prototype 345} 346 347/** 348 * @constructor 349 * @implements {WebInspector.OutputStream} 350 * @param {!WebInspector.TimelineModel} model 351 * @param {!{cancel: function()}} reader 352 * @param {!WebInspector.Progress} progress 353 */ 354WebInspector.TimelineModelLoader = function(model, reader, progress) 355{ 356 this._model = model; 357 this._reader = reader; 358 this._progress = progress; 359 this._buffer = ""; 360 this._firstChunk = true; 361} 362 363WebInspector.TimelineModelLoader.prototype = { 364 /** 365 * @param {string} chunk 366 */ 367 write: function(chunk) 368 { 369 var data = this._buffer + chunk; 370 var lastIndex = 0; 371 var index; 372 do { 373 index = lastIndex; 374 lastIndex = WebInspector.findBalancedCurlyBrackets(data, index); 375 } while (lastIndex !== -1) 376 377 var json = data.slice(0, index) + "]"; 378 this._buffer = data.slice(index); 379 380 if (!index) 381 return; 382 383 // Prepending "0" to turn string into valid JSON. 384 if (!this._firstChunk) 385 json = "[0" + json; 386 387 var items; 388 try { 389 items = /** @type {!Array.<!TimelineAgent.TimelineEvent>} */ (JSON.parse(json)); 390 } catch (e) { 391 WebInspector.showErrorMessage("Malformed timeline data."); 392 this._model.reset(); 393 this._reader.cancel(); 394 this._progress.done(); 395 return; 396 } 397 398 if (this._firstChunk) { 399 this._version = items[0]; 400 this._firstChunk = false; 401 this._model.reset(); 402 } 403 404 // Skip 0-th element - it is either version or 0. 405 for (var i = 1, size = items.length; i < size; ++i) 406 this._model._addRecord(items[i]); 407 }, 408 409 close: function() { } 410} 411 412/** 413 * @constructor 414 * @implements {WebInspector.OutputStreamDelegate} 415 * @param {!WebInspector.TimelineModel} model 416 * @param {!WebInspector.Progress} progress 417 */ 418WebInspector.TimelineModelLoadFromFileDelegate = function(model, progress) 419{ 420 this._model = model; 421 this._progress = progress; 422} 423 424WebInspector.TimelineModelLoadFromFileDelegate.prototype = { 425 onTransferStarted: function() 426 { 427 this._progress.setTitle(WebInspector.UIString("Loading\u2026")); 428 }, 429 430 /** 431 * @param {!WebInspector.ChunkedReader} reader 432 */ 433 onChunkTransferred: function(reader) 434 { 435 if (this._progress.isCanceled()) { 436 reader.cancel(); 437 this._progress.done(); 438 this._model.reset(); 439 return; 440 } 441 442 var totalSize = reader.fileSize(); 443 if (totalSize) { 444 this._progress.setTotalWork(totalSize); 445 this._progress.setWorked(reader.loadedSize()); 446 } 447 }, 448 449 onTransferFinished: function() 450 { 451 this._progress.done(); 452 }, 453 454 /** 455 * @param {!WebInspector.ChunkedReader} reader 456 */ 457 onError: function(reader, event) 458 { 459 this._progress.done(); 460 this._model.reset(); 461 switch (event.target.error.code) { 462 case FileError.NOT_FOUND_ERR: 463 WebInspector.showErrorMessage(WebInspector.UIString("File \"%s\" not found.", reader.fileName())); 464 break; 465 case FileError.NOT_READABLE_ERR: 466 WebInspector.showErrorMessage(WebInspector.UIString("File \"%s\" is not readable", reader.fileName())); 467 break; 468 case FileError.ABORT_ERR: 469 break; 470 default: 471 WebInspector.showErrorMessage(WebInspector.UIString("An error occurred while reading the file \"%s\"", reader.fileName())); 472 } 473 } 474} 475 476/** 477 * @constructor 478 */ 479WebInspector.TimelineSaver = function(stream) 480{ 481 this._stream = stream; 482} 483 484WebInspector.TimelineSaver.prototype = { 485 /** 486 * @param {!Array.<*>} records 487 * @param {string} version 488 */ 489 save: function(records, version) 490 { 491 this._records = records; 492 this._recordIndex = 0; 493 this._prologue = "[" + JSON.stringify(version); 494 495 this._writeNextChunk(this._stream); 496 }, 497 498 _writeNextChunk: function(stream) 499 { 500 const separator = ",\n"; 501 var data = []; 502 var length = 0; 503 504 if (this._prologue) { 505 data.push(this._prologue); 506 length += this._prologue.length; 507 delete this._prologue; 508 } else { 509 if (this._recordIndex === this._records.length) { 510 stream.close(); 511 return; 512 } 513 data.push(""); 514 } 515 while (this._recordIndex < this._records.length) { 516 var item = JSON.stringify(this._records[this._recordIndex]); 517 var itemLength = item.length + separator.length; 518 if (length + itemLength > WebInspector.TimelineModel.TransferChunkLengthBytes) 519 break; 520 length += itemLength; 521 data.push(item); 522 ++this._recordIndex; 523 } 524 if (this._recordIndex === this._records.length) 525 data.push(data.pop() + "]"); 526 stream.write(data.join(separator), this._writeNextChunk.bind(this)); 527 } 528} 529