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