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 */ 35 WebInspector.TimelineModel = function() 36 { 37 this._records = []; 38 this._stringPool = new StringPool(); 39 this._minimumRecordTime = -1; 40 this._maximumRecordTime = -1; 41 this._collectionEnabled = false; 42 43 WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.EventTypes.TimelineEventRecorded, this._onRecordAdded, this); 44 WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.EventTypes.TimelineStartEvent, this._onTimelineStarted, this); 45 } 46 47 WebInspector.TimelineModel.TransferChunkLengthBytes = 5000000; 48 49 WebInspector.TimelineModel.RecordType = { 50 Root: "Root", 51 Program: "Program", 52 EventDispatch: "EventDispatch", 53 54 BeginFrame: "BeginFrame", 55 ScheduleStyleRecalculation: "ScheduleStyleRecalculation", 56 RecalculateStyles: "RecalculateStyles", 57 InvalidateLayout: "InvalidateLayout", 58 Layout: "Layout", 59 PaintSetup: "PaintSetup", 60 Paint: "Paint", 61 Rasterize: "Rasterize", 62 ScrollLayer: "ScrollLayer", 63 DecodeImage: "DecodeImage", 64 ResizeImage: "ResizeImage", 65 CompositeLayers: "CompositeLayers", 66 67 ParseHTML: "ParseHTML", 68 69 TimerInstall: "TimerInstall", 70 TimerRemove: "TimerRemove", 71 TimerFire: "TimerFire", 72 73 XHRReadyStateChange: "XHRReadyStateChange", 74 XHRLoad: "XHRLoad", 75 EvaluateScript: "EvaluateScript", 76 77 MarkLoad: "MarkLoad", 78 MarkDOMContent: "MarkDOMContent", 79 80 TimeStamp: "TimeStamp", 81 Time: "Time", 82 TimeEnd: "TimeEnd", 83 84 ScheduleResourceRequest: "ScheduleResourceRequest", 85 ResourceSendRequest: "ResourceSendRequest", 86 ResourceReceiveResponse: "ResourceReceiveResponse", 87 ResourceReceivedData: "ResourceReceivedData", 88 ResourceFinish: "ResourceFinish", 89 90 FunctionCall: "FunctionCall", 91 GCEvent: "GCEvent", 92 93 RequestAnimationFrame: "RequestAnimationFrame", 94 CancelAnimationFrame: "CancelAnimationFrame", 95 FireAnimationFrame: "FireAnimationFrame", 96 97 WebSocketCreate : "WebSocketCreate", 98 WebSocketSendHandshakeRequest : "WebSocketSendHandshakeRequest", 99 WebSocketReceiveHandshakeResponse : "WebSocketReceiveHandshakeResponse", 100 WebSocketDestroy : "WebSocketDestroy", 101 } 102 103 WebInspector.TimelineModel.Events = { 104 RecordAdded: "RecordAdded", 105 RecordsCleared: "RecordsCleared" 106 } 107 108 WebInspector.TimelineModel.startTimeInSeconds = function(record) 109 { 110 return record.startTime / 1000; 111 } 112 113 WebInspector.TimelineModel.endTimeInSeconds = function(record) 114 { 115 return (typeof record.endTime === "undefined" ? record.startTime : record.endTime) / 1000; 116 } 117 118 WebInspector.TimelineModel.durationInSeconds = function(record) 119 { 120 return WebInspector.TimelineModel.endTimeInSeconds(record) - WebInspector.TimelineModel.startTimeInSeconds(record); 121 } 122 123 /** 124 * @param {Object} total 125 * @param {Object} rawRecord 126 */ 127 WebInspector.TimelineModel.aggregateTimeForRecord = function(total, rawRecord) 128 { 129 var childrenTime = 0; 130 var children = rawRecord["children"] || []; 131 for (var i = 0; i < children.length; ++i) { 132 WebInspector.TimelineModel.aggregateTimeForRecord(total, children[i]); 133 childrenTime += WebInspector.TimelineModel.durationInSeconds(children[i]); 134 } 135 var categoryName = WebInspector.TimelinePresentationModel.recordStyle(rawRecord).category.name; 136 var ownTime = WebInspector.TimelineModel.durationInSeconds(rawRecord) - childrenTime; 137 total[categoryName] = (total[categoryName] || 0) + ownTime; 138 } 139 140 /** 141 * @param {Object} total 142 * @param {Object} addend 143 */ 144 WebInspector.TimelineModel.aggregateTimeByCategory = function(total, addend) 145 { 146 for (var category in addend) 147 total[category] = (total[category] || 0) + addend[category]; 148 } 149 150 WebInspector.TimelineModel.prototype = { 151 /** 152 * @param {boolean=} includeDomCounters 153 */ 154 startRecord: function(includeDomCounters) 155 { 156 if (this._collectionEnabled) 157 return; 158 this.reset(); 159 var maxStackFrames = WebInspector.settings.timelineLimitStackFramesFlag.get() ? WebInspector.settings.timelineStackFramesToCapture.get() : 30; 160 WebInspector.timelineManager.start(maxStackFrames, includeDomCounters); 161 this._collectionEnabled = true; 162 }, 163 164 stopRecord: function() 165 { 166 if (!this._collectionEnabled) 167 return; 168 WebInspector.timelineManager.stop(); 169 this._collectionEnabled = false; 170 }, 171 172 get records() 173 { 174 return this._records; 175 }, 176 177 _onRecordAdded: function(event) 178 { 179 if (this._collectionEnabled) 180 this._addRecord(event.data); 181 }, 182 183 _addRecord: function(record) 184 { 185 this._stringPool.internObjectStrings(record); 186 this._records.push(record); 187 this._updateBoundaries(record); 188 this.dispatchEventToListeners(WebInspector.TimelineModel.Events.RecordAdded, record); 189 }, 190 191 /** 192 * @param {!Blob} file 193 * @param {!WebInspector.Progress} progress 194 */ 195 loadFromFile: function(file, progress) 196 { 197 var delegate = new WebInspector.TimelineModelLoadFromFileDelegate(this, progress); 198 var fileReader = this._createFileReader(file, delegate); 199 var loader = new WebInspector.TimelineModelLoader(this, fileReader, progress); 200 fileReader.start(loader); 201 }, 202 203 /** 204 * @param {string} url 205 */ 206 loadFromURL: function(url, progress) 207 { 208 var delegate = new WebInspector.TimelineModelLoadFromFileDelegate(this, progress); 209 var urlReader = new WebInspector.ChunkedXHRReader(url, delegate); 210 var loader = new WebInspector.TimelineModelLoader(this, urlReader, progress); 211 urlReader.start(loader); 212 }, 213 214 _createFileReader: function(file, delegate) 215 { 216 return new WebInspector.ChunkedFileReader(file, WebInspector.TimelineModel.TransferChunkLengthBytes, delegate); 217 }, 218 219 _createFileWriter: function(fileName, callback) 220 { 221 var stream = new WebInspector.FileOutputStream(); 222 stream.open(fileName, callback); 223 }, 224 225 saveToFile: function() 226 { 227 var now = new Date(); 228 var fileName = "TimelineRawData-" + now.toISO8601Compact() + ".json"; 229 function callback(stream) 230 { 231 var saver = new WebInspector.TimelineSaver(stream); 232 saver.save(this._records, window.navigator.appVersion); 233 } 234 this._createFileWriter(fileName, callback.bind(this)); 235 }, 236 237 reset: function() 238 { 239 this._records = []; 240 this._stringPool.reset(); 241 this._minimumRecordTime = -1; 242 this._maximumRecordTime = -1; 243 this.dispatchEventToListeners(WebInspector.TimelineModel.Events.RecordsCleared); 244 }, 245 246 minimumRecordTime: function() 247 { 248 return this._minimumRecordTime; 249 }, 250 251 maximumRecordTime: function() 252 { 253 return this._maximumRecordTime; 254 }, 255 256 _updateBoundaries: function(record) 257 { 258 var startTime = WebInspector.TimelineModel.startTimeInSeconds(record); 259 var endTime = WebInspector.TimelineModel.endTimeInSeconds(record); 260 261 if (this._minimumRecordTime === -1 || startTime < this._minimumRecordTime) 262 this._minimumRecordTime = startTime; 263 if (this._maximumRecordTime === -1 || endTime > this._maximumRecordTime) 264 this._maximumRecordTime = endTime; 265 }, 266 267 _onTimelineStarted: function(event) 268 { 269 if (event.data.timestampsBase) 270 this._timestampsBase = event.data.timestampsBase; 271 if (event.data.startTime) 272 this._startTime = event.data.startTime; 273 }, 274 275 /** 276 * @param {Object} rawRecord 277 */ 278 recordOffsetInSeconds: function(rawRecord) 279 { 280 return WebInspector.TimelineModel.startTimeInSeconds(rawRecord) - this._minimumRecordTime; 281 }, 282 283 __proto__: WebInspector.Object.prototype 284 } 285 286 /** 287 * @constructor 288 * @implements {WebInspector.OutputStream} 289 * @param {!WebInspector.TimelineModel} model 290 * @param {!{cancel: function()}} reader 291 * @param {!WebInspector.Progress} progress 292 */ 293 WebInspector.TimelineModelLoader = function(model, reader, progress) 294 { 295 this._model = model; 296 this._reader = reader; 297 this._progress = progress; 298 this._buffer = ""; 299 this._firstChunk = true; 300 } 301 302 WebInspector.TimelineModelLoader.prototype = { 303 /** 304 * @param {string} chunk 305 */ 306 write: function(chunk) 307 { 308 var data = this._buffer + chunk; 309 var lastIndex = 0; 310 var index; 311 do { 312 index = lastIndex; 313 lastIndex = WebInspector.findBalancedCurlyBrackets(data, index); 314 } while (lastIndex !== -1) 315 316 var json = data.slice(0, index) + "]"; 317 this._buffer = data.slice(index); 318 319 if (!index) 320 return; 321 322 // Prepending "0" to turn string into valid JSON. 323 if (!this._firstChunk) 324 json = "[0" + json; 325 326 var items; 327 try { 328 items = /** @type {Array} */ (JSON.parse(json)); 329 } catch (e) { 330 WebInspector.showErrorMessage("Malformed timeline data."); 331 this._model.reset(); 332 this._reader.cancel(); 333 this._progress.done(); 334 return; 335 } 336 337 if (this._firstChunk) { 338 this._version = items[0]; 339 this._firstChunk = false; 340 this._model.reset(); 341 } 342 343 // Skip 0-th element - it is either version or 0. 344 for (var i = 1, size = items.length; i < size; ++i) 345 this._model._addRecord(items[i]); 346 }, 347 348 close: function() { } 349 } 350 351 /** 352 * @constructor 353 * @implements {WebInspector.OutputStreamDelegate} 354 * @param {!WebInspector.TimelineModel} model 355 * @param {!WebInspector.Progress} progress 356 */ 357 WebInspector.TimelineModelLoadFromFileDelegate = function(model, progress) 358 { 359 this._model = model; 360 this._progress = progress; 361 } 362 363 WebInspector.TimelineModelLoadFromFileDelegate.prototype = { 364 onTransferStarted: function() 365 { 366 this._progress.setTitle(WebInspector.UIString("Loading\u2026")); 367 }, 368 369 /** 370 * @param {WebInspector.ChunkedReader} reader 371 */ 372 onChunkTransferred: function(reader) 373 { 374 if (this._progress.isCanceled()) { 375 reader.cancel(); 376 this._progress.done(); 377 this._model.reset(); 378 return; 379 } 380 381 var totalSize = reader.fileSize(); 382 if (totalSize) { 383 this._progress.setTotalWork(totalSize); 384 this._progress.setWorked(reader.loadedSize()); 385 } 386 }, 387 388 onTransferFinished: function() 389 { 390 this._progress.done(); 391 }, 392 393 /** 394 * @param {WebInspector.ChunkedReader} reader 395 */ 396 onError: function(reader, event) 397 { 398 this._progress.done(); 399 this._model.reset(); 400 switch (event.target.error.code) { 401 case FileError.NOT_FOUND_ERR: 402 WebInspector.showErrorMessage(WebInspector.UIString("File \"%s\" not found.", reader.fileName())); 403 break; 404 case FileError.NOT_READABLE_ERR: 405 WebInspector.showErrorMessage(WebInspector.UIString("File \"%s\" is not readable", reader.fileName())); 406 break; 407 case FileError.ABORT_ERR: 408 break; 409 default: 410 WebInspector.showErrorMessage(WebInspector.UIString("An error occurred while reading the file \"%s\"", reader.fileName())); 411 } 412 } 413 } 414 415 /** 416 * @constructor 417 */ 418 WebInspector.TimelineSaver = function(stream) 419 { 420 this._stream = stream; 421 } 422 423 WebInspector.TimelineSaver.prototype = { 424 /** 425 * @param {Array} records 426 * @param {string} version 427 */ 428 save: function(records, version) 429 { 430 this._records = records; 431 this._recordIndex = 0; 432 this._prologue = "[" + JSON.stringify(version); 433 434 this._writeNextChunk(this._stream); 435 }, 436 437 _writeNextChunk: function(stream) 438 { 439 const separator = ",\n"; 440 var data = []; 441 var length = 0; 442 443 if (this._prologue) { 444 data.push(this._prologue); 445 length += this._prologue.length; 446 delete this._prologue; 447 } else { 448 if (this._recordIndex === this._records.length) { 449 stream.close(); 450 return; 451 } 452 data.push(""); 453 } 454 while (this._recordIndex < this._records.length) { 455 var item = JSON.stringify(this._records[this._recordIndex]); 456 var itemLength = item.length + separator.length; 457 if (length + itemLength > WebInspector.TimelineModel.TransferChunkLengthBytes) 458 break; 459 length += itemLength; 460 data.push(item); 461 ++this._recordIndex; 462 } 463 if (this._recordIndex === this._records.length) 464 data.push(data.pop() + "]"); 465 stream.write(data.join(separator), this._writeNextChunk.bind(this)); 466 } 467 } 468