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