| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * @constructor |
| * @extends {WebInspector.TimelineModel} |
| * @param {!WebInspector.TimelineManager} timelineManager |
| */ |
| WebInspector.TimelineModelImpl = function(timelineManager) |
| { |
| WebInspector.TimelineModel.call(this, timelineManager.target()); |
| this._timelineManager = timelineManager; |
| this._filters = []; |
| this._bindings = new WebInspector.TimelineModelImpl.InterRecordBindings(); |
| |
| this.reset(); |
| |
| this._timelineManager.addEventListener(WebInspector.TimelineManager.EventTypes.TimelineEventRecorded, this._onRecordAdded, this); |
| this._timelineManager.addEventListener(WebInspector.TimelineManager.EventTypes.TimelineStarted, this._onStarted, this); |
| this._timelineManager.addEventListener(WebInspector.TimelineManager.EventTypes.TimelineStopped, this._onStopped, this); |
| this._timelineManager.addEventListener(WebInspector.TimelineManager.EventTypes.TimelineProgress, this._onProgress, this); |
| } |
| |
| WebInspector.TimelineModelImpl.TransferChunkLengthBytes = 5000000; |
| |
| WebInspector.TimelineModelImpl.prototype = { |
| /** |
| * @return {boolean} |
| */ |
| loadedFromFile: function() |
| { |
| return this._loadedFromFile; |
| }, |
| |
| /** |
| * @param {boolean} captureStacks |
| * @param {boolean} captureMemory |
| */ |
| startRecording: function(captureStacks, captureMemory) |
| { |
| this._clientInitiatedRecording = true; |
| this.reset(); |
| var maxStackFrames = captureStacks ? 30 : 0; |
| var includeGPUEvents = WebInspector.experimentsSettings.gpuTimeline.isEnabled(); |
| var liveEvents = [ WebInspector.TimelineModel.RecordType.BeginFrame, |
| WebInspector.TimelineModel.RecordType.DrawFrame, |
| WebInspector.TimelineModel.RecordType.RequestMainThreadFrame, |
| WebInspector.TimelineModel.RecordType.ActivateLayerTree ]; |
| this._timelineManager.start(maxStackFrames, WebInspector.experimentsSettings.timelineNoLiveUpdate.isEnabled(), liveEvents.join(","), captureMemory, includeGPUEvents, this._fireRecordingStarted.bind(this)); |
| }, |
| |
| stopRecording: function() |
| { |
| if (!this._clientInitiatedRecording) { |
| this._timelineManager.start(undefined, undefined, undefined, undefined, undefined, stopTimeline.bind(this)); |
| return; |
| } |
| |
| /** |
| * Console started this one and we are just sniffing it. Initiate recording so that we |
| * could stop it. |
| * @this {WebInspector.TimelineModelImpl} |
| */ |
| function stopTimeline() |
| { |
| this._timelineManager.stop(this._fireRecordingStopped.bind(this)); |
| } |
| |
| this._clientInitiatedRecording = false; |
| this._timelineManager.stop(this._fireRecordingStopped.bind(this)); |
| }, |
| |
| /** |
| * @return {!Array.<!WebInspector.TimelineModel.Record>} |
| */ |
| records: function() |
| { |
| return this._records; |
| }, |
| |
| /** |
| * @param {!WebInspector.Event} event |
| */ |
| _onRecordAdded: function(event) |
| { |
| if (this._collectionEnabled) |
| this._addRecord(/** @type {!TimelineAgent.TimelineEvent} */(event.data)); |
| }, |
| |
| /** |
| * @param {!WebInspector.Event} event |
| */ |
| _onStarted: function(event) |
| { |
| if (event.data) { |
| // Started from console. |
| this._fireRecordingStarted(); |
| } |
| }, |
| |
| /** |
| * @param {!WebInspector.Event} event |
| */ |
| _onStopped: function(event) |
| { |
| // If we were buffering events, discard those that got through, the real ones are coming! |
| if (WebInspector.experimentsSettings.timelineNoLiveUpdate.isEnabled()) |
| this.reset(); |
| if (event.data) { |
| // Stopped from console. |
| this._fireRecordingStopped(null, null); |
| } |
| }, |
| |
| /** |
| * @param {!WebInspector.Event} event |
| */ |
| _onProgress: function(event) |
| { |
| this.dispatchEventToListeners(WebInspector.TimelineModel.Events.RecordingProgress, event.data); |
| }, |
| |
| _fireRecordingStarted: function() |
| { |
| this._collectionEnabled = true; |
| this.dispatchEventToListeners(WebInspector.TimelineModel.Events.RecordingStarted); |
| }, |
| |
| /** |
| * @param {?Protocol.Error} error |
| * @param {?ProfilerAgent.CPUProfile} cpuProfile |
| */ |
| _fireRecordingStopped: function(error, cpuProfile) |
| { |
| this._collectionEnabled = false; |
| if (cpuProfile) |
| WebInspector.TimelineJSProfileProcessor.mergeJSProfileIntoTimeline(this, cpuProfile); |
| this.dispatchEventToListeners(WebInspector.TimelineModel.Events.RecordingStopped); |
| }, |
| |
| /** |
| * @param {!TimelineAgent.TimelineEvent} payload |
| */ |
| _addRecord: function(payload) |
| { |
| this._internStrings(payload); |
| this._payloads.push(payload); |
| |
| var record = this._innerAddRecord(payload, null); |
| this._updateBoundaries(record); |
| this._records.push(record); |
| if (record.type() === WebInspector.TimelineModel.RecordType.Program) |
| this._mainThreadTasks.push(record); |
| if (record.type() === WebInspector.TimelineModel.RecordType.GPUTask) |
| this._gpuThreadTasks.push(record); |
| |
| this.dispatchEventToListeners(WebInspector.TimelineModel.Events.RecordAdded, record); |
| }, |
| |
| /** |
| * @param {!TimelineAgent.TimelineEvent} payload |
| * @param {?WebInspector.TimelineModel.Record} parentRecord |
| * @return {!WebInspector.TimelineModel.Record} |
| * @this {!WebInspector.TimelineModel} |
| */ |
| _innerAddRecord: function(payload, parentRecord) |
| { |
| var record = new WebInspector.TimelineModel.RecordImpl(this, payload, parentRecord); |
| if (WebInspector.TimelineUIUtils.isEventDivider(record)) |
| this._eventDividerRecords.push(record); |
| |
| for (var i = 0; payload.children && i < payload.children.length; ++i) |
| this._innerAddRecord.call(this, payload.children[i], record); |
| |
| record._calculateAggregatedStats(); |
| if (parentRecord) |
| parentRecord._selfTime -= record.endTime() - record.startTime(); |
| return record; |
| }, |
| |
| /** |
| * @param {!Blob} file |
| * @param {!WebInspector.Progress} progress |
| */ |
| loadFromFile: function(file, progress) |
| { |
| var delegate = new WebInspector.TimelineModelLoadFromFileDelegate(this, progress); |
| var fileReader = this._createFileReader(file, delegate); |
| var loader = new WebInspector.TimelineModelLoader(this, fileReader, progress); |
| fileReader.start(loader); |
| }, |
| |
| /** |
| * @param {string} url |
| * @param {!WebInspector.Progress} progress |
| */ |
| loadFromURL: function(url, progress) |
| { |
| var delegate = new WebInspector.TimelineModelLoadFromFileDelegate(this, progress); |
| var urlReader = new WebInspector.ChunkedXHRReader(url, delegate); |
| var loader = new WebInspector.TimelineModelLoader(this, urlReader, progress); |
| urlReader.start(loader); |
| }, |
| |
| _createFileReader: function(file, delegate) |
| { |
| return new WebInspector.ChunkedFileReader(file, WebInspector.TimelineModelImpl.TransferChunkLengthBytes, delegate); |
| }, |
| |
| _createFileWriter: function() |
| { |
| return new WebInspector.FileOutputStream(); |
| }, |
| |
| saveToFile: function() |
| { |
| var now = new Date(); |
| var fileName = "TimelineRawData-" + now.toISO8601Compact() + ".json"; |
| var stream = this._createFileWriter(); |
| |
| /** |
| * @param {boolean} accepted |
| * @this {WebInspector.TimelineModelImpl} |
| */ |
| function callback(accepted) |
| { |
| if (!accepted) |
| return; |
| var saver = new WebInspector.TimelineSaver(stream); |
| saver.save(this._payloads, window.navigator.appVersion); |
| } |
| stream.open(fileName, callback.bind(this)); |
| }, |
| |
| reset: function() |
| { |
| this._loadedFromFile = false; |
| this._payloads = []; |
| this._stringPool = {}; |
| this._bindings._reset(); |
| WebInspector.TimelineModel.prototype.reset.call(this); |
| }, |
| |
| /** |
| * @param {!TimelineAgent.TimelineEvent} record |
| */ |
| _internStrings: function(record) |
| { |
| for (var name in record) { |
| var value = record[name]; |
| if (typeof value !== "string") |
| continue; |
| |
| var interned = this._stringPool[value]; |
| if (typeof interned === "string") |
| record[name] = interned; |
| else |
| this._stringPool[value] = value; |
| } |
| |
| var children = record.children; |
| for (var i = 0; children && i < children.length; ++i) |
| this._internStrings(children[i]); |
| }, |
| |
| __proto__: WebInspector.TimelineModel.prototype |
| } |
| |
| |
| /** |
| * @constructor |
| */ |
| WebInspector.TimelineModelImpl.InterRecordBindings = function() { |
| this._reset(); |
| } |
| |
| WebInspector.TimelineModelImpl.InterRecordBindings.prototype = { |
| _reset: function() |
| { |
| this._sendRequestRecords = {}; |
| this._timerRecords = {}; |
| this._requestAnimationFrameRecords = {}; |
| this._layoutInvalidate = {}; |
| this._lastScheduleStyleRecalculation = {}; |
| this._webSocketCreateRecords = {}; |
| } |
| } |
| |
| /** |
| * @constructor |
| * @implements {WebInspector.TimelineModel.Record} |
| * @param {!WebInspector.TimelineModel} model |
| * @param {!TimelineAgent.TimelineEvent} timelineEvent |
| * @param {?WebInspector.TimelineModel.Record} parentRecord |
| */ |
| WebInspector.TimelineModel.RecordImpl = function(model, timelineEvent, parentRecord) |
| { |
| this._model = model; |
| var bindings = this._model._bindings; |
| this._aggregatedStats = {}; |
| this._record = timelineEvent; |
| this._children = []; |
| if (parentRecord) { |
| this.parent = parentRecord; |
| parentRecord.children().push(this); |
| } |
| |
| this._selfTime = this.endTime() - this.startTime(); |
| |
| var recordTypes = WebInspector.TimelineModel.RecordType; |
| switch (timelineEvent.type) { |
| case recordTypes.ResourceSendRequest: |
| // Make resource receive record last since request was sent; make finish record last since response received. |
| bindings._sendRequestRecords[timelineEvent.data["requestId"]] = this; |
| break; |
| |
| case recordTypes.ResourceReceiveResponse: |
| case recordTypes.ResourceReceivedData: |
| case recordTypes.ResourceFinish: |
| this._initiator = bindings._sendRequestRecords[timelineEvent.data["requestId"]]; |
| break; |
| |
| case recordTypes.TimerInstall: |
| bindings._timerRecords[timelineEvent.data["timerId"]] = this; |
| break; |
| |
| case recordTypes.TimerFire: |
| this._initiator = bindings._timerRecords[timelineEvent.data["timerId"]]; |
| break; |
| |
| case recordTypes.RequestAnimationFrame: |
| bindings._requestAnimationFrameRecords[timelineEvent.data["id"]] = this; |
| break; |
| |
| case recordTypes.FireAnimationFrame: |
| this._initiator = bindings._requestAnimationFrameRecords[timelineEvent.data["id"]]; |
| break; |
| |
| case recordTypes.ScheduleStyleRecalculation: |
| bindings._lastScheduleStyleRecalculation[this.frameId()] = this; |
| break; |
| |
| case recordTypes.RecalculateStyles: |
| this._initiator = bindings._lastScheduleStyleRecalculation[this.frameId()]; |
| break; |
| |
| case recordTypes.InvalidateLayout: |
| // Consider style recalculation as a reason for layout invalidation, |
| // but only if we had no earlier layout invalidation records. |
| var layoutInitator = this; |
| if (!bindings._layoutInvalidate[this.frameId()] && parentRecord.type() === recordTypes.RecalculateStyles) |
| layoutInitator = parentRecord._initiator; |
| bindings._layoutInvalidate[this.frameId()] = layoutInitator; |
| break; |
| |
| case recordTypes.Layout: |
| this._initiator = bindings._layoutInvalidate[this.frameId()]; |
| bindings._layoutInvalidate[this.frameId()] = null; |
| if (this.stackTrace()) |
| this.addWarning(WebInspector.UIString("Forced synchronous layout is a possible performance bottleneck.")); |
| break; |
| |
| case recordTypes.WebSocketCreate: |
| bindings._webSocketCreateRecords[timelineEvent.data["identifier"]] = this; |
| break; |
| |
| case recordTypes.WebSocketSendHandshakeRequest: |
| case recordTypes.WebSocketReceiveHandshakeResponse: |
| case recordTypes.WebSocketDestroy: |
| this._initiator = bindings._webSocketCreateRecords[timelineEvent.data["identifier"]]; |
| break; |
| } |
| } |
| |
| WebInspector.TimelineModel.RecordImpl.prototype = { |
| /** |
| * @return {?Array.<!ConsoleAgent.CallFrame>} |
| */ |
| callSiteStackTrace: function() |
| { |
| return this._initiator ? this._initiator.stackTrace() : null; |
| }, |
| |
| /** |
| * @return {?WebInspector.TimelineModel.Record} |
| */ |
| initiator: function() |
| { |
| return this._initiator; |
| }, |
| |
| /** |
| * @return {!WebInspector.Target} |
| */ |
| target: function() |
| { |
| return this._model.target(); |
| }, |
| |
| /** |
| * @return {number} |
| */ |
| selfTime: function() |
| { |
| return this._selfTime; |
| }, |
| |
| /** |
| * @return {!Array.<!WebInspector.TimelineModel.Record>} |
| */ |
| children: function() |
| { |
| return this._children; |
| }, |
| |
| /** |
| * @return {!WebInspector.TimelineCategory} |
| */ |
| category: function() |
| { |
| return WebInspector.TimelineUIUtils.categoryForRecord(this); |
| }, |
| |
| /** |
| * @return {string} |
| */ |
| title: function() |
| { |
| return WebInspector.TimelineUIUtils.recordTitle(this, this._model); |
| }, |
| |
| /** |
| * @return {number} |
| */ |
| startTime: function() |
| { |
| return this._record.startTime; |
| }, |
| |
| /** |
| * @return {string|undefined} |
| */ |
| thread: function() |
| { |
| return this._record.thread; |
| }, |
| |
| /** |
| * @return {number} |
| */ |
| endTime: function() |
| { |
| return this._endTime || this._record.endTime || this._record.startTime; |
| }, |
| |
| /** |
| * @param {number} endTime |
| */ |
| setEndTime: function(endTime) |
| { |
| this._endTime = endTime; |
| }, |
| |
| /** |
| * @return {!Object} |
| */ |
| data: function() |
| { |
| return this._record.data; |
| }, |
| |
| /** |
| * @return {string} |
| */ |
| type: function() |
| { |
| return this._record.type; |
| }, |
| |
| /** |
| * @return {string} |
| */ |
| frameId: function() |
| { |
| return this._record.frameId || ""; |
| }, |
| |
| /** |
| * @return {?Array.<!ConsoleAgent.CallFrame>} |
| */ |
| stackTrace: function() |
| { |
| if (this._record.stackTrace && this._record.stackTrace.length) |
| return this._record.stackTrace; |
| return null; |
| }, |
| |
| /** |
| * @param {string} key |
| * @return {?Object} |
| */ |
| getUserObject: function(key) |
| { |
| if (!this._userObjects) |
| return null; |
| return this._userObjects.get(key); |
| }, |
| |
| /** |
| * @param {string} key |
| * @param {?Object|undefined} value |
| */ |
| setUserObject: function(key, value) |
| { |
| if (!this._userObjects) |
| this._userObjects = new StringMap(); |
| this._userObjects.put(key, value); |
| }, |
| |
| _calculateAggregatedStats: function() |
| { |
| this._aggregatedStats = {}; |
| |
| for (var index = this._children.length; index; --index) { |
| var child = this._children[index - 1]; |
| for (var category in child._aggregatedStats) |
| this._aggregatedStats[category] = (this._aggregatedStats[category] || 0) + child._aggregatedStats[category]; |
| } |
| this._aggregatedStats[this.category().name] = (this._aggregatedStats[this.category().name] || 0) + this._selfTime; |
| }, |
| |
| /** |
| * @return {!Object.<string, number>} |
| */ |
| aggregatedStats: function() |
| { |
| return this._aggregatedStats; |
| }, |
| |
| /** |
| * @param {string} message |
| */ |
| addWarning: function(message) |
| { |
| if (!this._warnings) |
| this._warnings = []; |
| this._warnings.push(message); |
| }, |
| |
| /** |
| * @return {?Array.<string>} |
| */ |
| warnings: function() |
| { |
| return this._warnings; |
| }, |
| |
| /** |
| * @param {!RegExp} regExp |
| * @return {boolean} |
| */ |
| testContentMatching: function(regExp) |
| { |
| var tokens = [this.title()]; |
| for (var key in this._record.data) |
| tokens.push(this._record.data[key]) |
| return regExp.test(tokens.join("|")); |
| } |
| } |
| |
| /** |
| * @constructor |
| * @implements {WebInspector.OutputStream} |
| * @param {!WebInspector.TimelineModel} model |
| * @param {!{cancel: function()}} reader |
| * @param {!WebInspector.Progress} progress |
| */ |
| WebInspector.TimelineModelLoader = function(model, reader, progress) |
| { |
| this._model = model; |
| this._reader = reader; |
| this._progress = progress; |
| this._buffer = ""; |
| this._firstChunk = true; |
| } |
| |
| WebInspector.TimelineModelLoader.prototype = { |
| /** |
| * @param {string} chunk |
| */ |
| write: function(chunk) |
| { |
| var data = this._buffer + chunk; |
| var lastIndex = 0; |
| var index; |
| do { |
| index = lastIndex; |
| lastIndex = WebInspector.TextUtils.findBalancedCurlyBrackets(data, index); |
| } while (lastIndex !== -1) |
| |
| var json = data.slice(0, index) + "]"; |
| this._buffer = data.slice(index); |
| |
| if (!index) |
| return; |
| |
| // Prepending "0" to turn string into valid JSON. |
| if (!this._firstChunk) |
| json = "[0" + json; |
| |
| var items; |
| try { |
| items = /** @type {!Array.<!TimelineAgent.TimelineEvent>} */ (JSON.parse(json)); |
| } catch (e) { |
| WebInspector.messageSink.addErrorMessage("Malformed timeline data.", true); |
| this._model.reset(); |
| this._reader.cancel(); |
| this._progress.done(); |
| return; |
| } |
| |
| if (this._firstChunk) { |
| this._version = items[0]; |
| this._firstChunk = false; |
| this._model.reset(); |
| } |
| |
| // Skip 0-th element - it is either version or 0. |
| for (var i = 1, size = items.length; i < size; ++i) |
| this._model._addRecord(items[i]); |
| }, |
| |
| close: function() |
| { |
| this._model._loadedFromFile = true; |
| } |
| } |
| |
| /** |
| * @constructor |
| * @implements {WebInspector.OutputStreamDelegate} |
| * @param {!WebInspector.TimelineModel} model |
| * @param {!WebInspector.Progress} progress |
| */ |
| WebInspector.TimelineModelLoadFromFileDelegate = function(model, progress) |
| { |
| this._model = model; |
| this._progress = progress; |
| } |
| |
| WebInspector.TimelineModelLoadFromFileDelegate.prototype = { |
| onTransferStarted: function() |
| { |
| this._progress.setTitle(WebInspector.UIString("Loading\u2026")); |
| }, |
| |
| /** |
| * @param {!WebInspector.ChunkedReader} reader |
| */ |
| onChunkTransferred: function(reader) |
| { |
| if (this._progress.isCanceled()) { |
| reader.cancel(); |
| this._progress.done(); |
| this._model.reset(); |
| return; |
| } |
| |
| var totalSize = reader.fileSize(); |
| if (totalSize) { |
| this._progress.setTotalWork(totalSize); |
| this._progress.setWorked(reader.loadedSize()); |
| } |
| }, |
| |
| onTransferFinished: function() |
| { |
| this._progress.done(); |
| }, |
| |
| /** |
| * @param {!WebInspector.ChunkedReader} reader |
| * @param {!Event} event |
| */ |
| onError: function(reader, event) |
| { |
| this._progress.done(); |
| this._model.reset(); |
| switch (event.target.error.code) { |
| case FileError.NOT_FOUND_ERR: |
| WebInspector.messageSink.addErrorMessage(WebInspector.UIString("File \"%s\" not found.", reader.fileName()), true); |
| break; |
| case FileError.NOT_READABLE_ERR: |
| WebInspector.messageSink.addErrorMessage(WebInspector.UIString("File \"%s\" is not readable", reader.fileName()), true); |
| break; |
| case FileError.ABORT_ERR: |
| break; |
| default: |
| WebInspector.messageSink.addErrorMessage(WebInspector.UIString("An error occurred while reading the file \"%s\"", reader.fileName()), true); |
| } |
| } |
| } |
| |
| /** |
| * @constructor |
| * @param {!WebInspector.OutputStream} stream |
| */ |
| WebInspector.TimelineSaver = function(stream) |
| { |
| this._stream = stream; |
| } |
| |
| WebInspector.TimelineSaver.prototype = { |
| /** |
| * @param {!Array.<*>} payloads |
| * @param {string} version |
| */ |
| save: function(payloads, version) |
| { |
| this._payloads = payloads; |
| this._recordIndex = 0; |
| this._prologue = "[" + JSON.stringify(version); |
| |
| this._writeNextChunk(this._stream); |
| }, |
| |
| _writeNextChunk: function(stream) |
| { |
| const separator = ",\n"; |
| var data = []; |
| var length = 0; |
| |
| if (this._prologue) { |
| data.push(this._prologue); |
| length += this._prologue.length; |
| delete this._prologue; |
| } else { |
| if (this._recordIndex === this._payloads.length) { |
| stream.close(); |
| return; |
| } |
| data.push(""); |
| } |
| while (this._recordIndex < this._payloads.length) { |
| var item = JSON.stringify(this._payloads[this._recordIndex]); |
| var itemLength = item.length + separator.length; |
| if (length + itemLength > WebInspector.TimelineModelImpl.TransferChunkLengthBytes) |
| break; |
| length += itemLength; |
| data.push(item); |
| ++this._recordIndex; |
| } |
| if (this._recordIndex === this._payloads.length) |
| data.push(data.pop() + "]"); |
| stream.write(data.join(separator), this._writeNextChunk.bind(this)); |
| } |
| } |