• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */
10WebInspector.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
25WebInspector.TimelineModelImpl.TransferChunkLengthBytes = 5000000;
26
27WebInspector.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 */
275WebInspector.TimelineModelImpl.InterRecordBindings = function() {
276    this._reset();
277}
278
279WebInspector.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 */
298WebInspector.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
377WebInspector.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 */
560WebInspector.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
569WebInspector.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 */
627WebInspector.TimelineModelLoadFromFileDelegate = function(model, progress)
628{
629    this._model = model;
630    this._progress = progress;
631}
632
633WebInspector.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 */
690WebInspector.TimelineSaver = function(stream)
691{
692    this._stream = stream;
693}
694
695WebInspector.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