Home | History | Annotate | Download | only in front_end
      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 
     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 
     47 WebInspector.TimelineModel.TransferChunkLengthBytes = 5000000;
     48 
     49 WebInspector.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 
    108 WebInspector.TimelineModel.Events = {
    109     RecordAdded: "RecordAdded",
    110     RecordsCleared: "RecordsCleared",
    111     RecordingStarted: "RecordingStarted",
    112     RecordingStopped: "RecordingStopped"
    113 }
    114 
    115 WebInspector.TimelineModel.startTimeInSeconds = function(record)
    116 {
    117     return record.startTime / 1000;
    118 }
    119 
    120 WebInspector.TimelineModel.endTimeInSeconds = function(record)
    121 {
    122     return (record.endTime || record.startTime) / 1000;
    123 }
    124 
    125 WebInspector.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  */
    134 WebInspector.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  */
    151 WebInspector.TimelineModel.aggregateTimeByCategory = function(total, addend)
    152 {
    153     for (var category in addend)
    154         total[category] = (total[category] || 0) + addend[category];
    155 }
    156 
    157 WebInspector.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  */
    354 WebInspector.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 
    363 WebInspector.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  */
    418 WebInspector.TimelineModelLoadFromFileDelegate = function(model, progress)
    419 {
    420     this._model = model;
    421     this._progress = progress;
    422 }
    423 
    424 WebInspector.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  */
    479 WebInspector.TimelineSaver = function(stream)
    480 {
    481     this._stream = stream;
    482 }
    483 
    484 WebInspector.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