Home | History | Annotate | Download | only in timeline
      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