blob: 62d812132c22afa7b634b6bb17f4437f3e3ca542 [file] [log] [blame]
/*
* Copyright (C) 2012 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @constructor
* @extends {WebInspector.Object}
*/
WebInspector.TimelineModel = function()
{
this._records = [];
this._stringPool = new StringPool();
this._minimumRecordTime = -1;
this._maximumRecordTime = -1;
WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.EventTypes.TimelineEventRecorded, this._onRecordAdded, this);
WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.EventTypes.TimelineStarted, this._onStarted, this);
WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.EventTypes.TimelineStopped, this._onStopped, this);
}
WebInspector.TimelineModel.TransferChunkLengthBytes = 5000000;
WebInspector.TimelineModel.RecordType = {
Root: "Root",
Program: "Program",
EventDispatch: "EventDispatch",
BeginFrame: "BeginFrame",
ScheduleStyleRecalculation: "ScheduleStyleRecalculation",
RecalculateStyles: "RecalculateStyles",
InvalidateLayout: "InvalidateLayout",
Layout: "Layout",
PaintSetup: "PaintSetup",
Paint: "Paint",
Rasterize: "Rasterize",
ScrollLayer: "ScrollLayer",
DecodeImage: "DecodeImage",
ResizeImage: "ResizeImage",
CompositeLayers: "CompositeLayers",
ParseHTML: "ParseHTML",
TimerInstall: "TimerInstall",
TimerRemove: "TimerRemove",
TimerFire: "TimerFire",
XHRReadyStateChange: "XHRReadyStateChange",
XHRLoad: "XHRLoad",
EvaluateScript: "EvaluateScript",
MarkLoad: "MarkLoad",
MarkDOMContent: "MarkDOMContent",
TimeStamp: "TimeStamp",
Time: "Time",
TimeEnd: "TimeEnd",
ScheduleResourceRequest: "ScheduleResourceRequest",
ResourceSendRequest: "ResourceSendRequest",
ResourceReceiveResponse: "ResourceReceiveResponse",
ResourceReceivedData: "ResourceReceivedData",
ResourceFinish: "ResourceFinish",
FunctionCall: "FunctionCall",
GCEvent: "GCEvent",
RequestAnimationFrame: "RequestAnimationFrame",
CancelAnimationFrame: "CancelAnimationFrame",
FireAnimationFrame: "FireAnimationFrame",
WebSocketCreate : "WebSocketCreate",
WebSocketSendHandshakeRequest : "WebSocketSendHandshakeRequest",
WebSocketReceiveHandshakeResponse : "WebSocketReceiveHandshakeResponse",
WebSocketDestroy : "WebSocketDestroy",
}
WebInspector.TimelineModel.Events = {
RecordAdded: "RecordAdded",
RecordsCleared: "RecordsCleared",
RecordingStarted: "RecordingStarted",
RecordingStopped: "RecordingStopped"
}
WebInspector.TimelineModel.startTimeInSeconds = function(record)
{
return record.startTime / 1000;
}
WebInspector.TimelineModel.endTimeInSeconds = function(record)
{
return (typeof record.endTime === "undefined" ? record.startTime : record.endTime) / 1000;
}
WebInspector.TimelineModel.durationInSeconds = function(record)
{
return WebInspector.TimelineModel.endTimeInSeconds(record) - WebInspector.TimelineModel.startTimeInSeconds(record);
}
/**
* @param {Object} total
* @param {Object} rawRecord
*/
WebInspector.TimelineModel.aggregateTimeForRecord = function(total, rawRecord)
{
var childrenTime = 0;
var children = rawRecord["children"] || [];
for (var i = 0; i < children.length; ++i) {
WebInspector.TimelineModel.aggregateTimeForRecord(total, children[i]);
childrenTime += WebInspector.TimelineModel.durationInSeconds(children[i]);
}
var categoryName = WebInspector.TimelinePresentationModel.recordStyle(rawRecord).category.name;
var ownTime = WebInspector.TimelineModel.durationInSeconds(rawRecord) - childrenTime;
total[categoryName] = (total[categoryName] || 0) + ownTime;
}
/**
* @param {Object} total
* @param {Object} addend
*/
WebInspector.TimelineModel.aggregateTimeByCategory = function(total, addend)
{
for (var category in addend)
total[category] = (total[category] || 0) + addend[category];
}
WebInspector.TimelineModel.prototype = {
/**
* @param {boolean=} includeDomCounters
*/
startRecording: function(includeDomCounters)
{
this._clientInitiatedRecording = true;
this.reset();
var maxStackFrames = WebInspector.settings.timelineLimitStackFramesFlag.get() ? WebInspector.settings.timelineStackFramesToCapture.get() : 30;
WebInspector.timelineManager.start(maxStackFrames, includeDomCounters, this._fireRecordingStarted.bind(this));
},
stopRecording: function()
{
if (!this._clientInitiatedRecording) {
// Console started this one and we are just sniffing it. Initiate recording so that we
// could stop it.
function stopTimeline()
{
WebInspector.timelineManager.stop(this._fireRecordingStopped.bind(this));
}
WebInspector.timelineManager.start(undefined, undefined, stopTimeline.bind(this));
return;
}
this._clientInitiatedRecording = false;
WebInspector.timelineManager.stop(this._fireRecordingStopped.bind(this));
},
get records()
{
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 (event.data) {
// Stopped from console.
this._fireRecordingStopped();
}
},
_fireRecordingStarted: function()
{
this._collectionEnabled = true;
this.dispatchEventToListeners(WebInspector.TimelineModel.Events.RecordingStarted);
},
_fireRecordingStopped: function()
{
this._collectionEnabled = false;
this.dispatchEventToListeners(WebInspector.TimelineModel.Events.RecordingStopped);
},
/**
* @param {TimelineAgent.TimelineEvent} record
*/
_addRecord: function(record)
{
this._stringPool.internObjectStrings(record);
this._records.push(record);
this._updateBoundaries(record);
this.dispatchEventToListeners(WebInspector.TimelineModel.Events.RecordAdded, 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
*/
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.TimelineModel.TransferChunkLengthBytes, delegate);
},
_createFileWriter: function(fileName, callback)
{
var stream = new WebInspector.FileOutputStream();
stream.open(fileName, callback);
},
saveToFile: function()
{
var now = new Date();
var fileName = "TimelineRawData-" + now.toISO8601Compact() + ".json";
function callback(stream)
{
var saver = new WebInspector.TimelineSaver(stream);
saver.save(this._records, window.navigator.appVersion);
}
this._createFileWriter(fileName, callback.bind(this));
},
reset: function()
{
this._records = [];
this._stringPool.reset();
this._minimumRecordTime = -1;
this._maximumRecordTime = -1;
this.dispatchEventToListeners(WebInspector.TimelineModel.Events.RecordsCleared);
},
minimumRecordTime: function()
{
return this._minimumRecordTime;
},
maximumRecordTime: function()
{
return this._maximumRecordTime;
},
/**
* @param {TimelineAgent.TimelineEvent} record
*/
_updateBoundaries: function(record)
{
var startTime = WebInspector.TimelineModel.startTimeInSeconds(record);
var endTime = WebInspector.TimelineModel.endTimeInSeconds(record);
if (this._minimumRecordTime === -1 || startTime < this._minimumRecordTime)
this._minimumRecordTime = startTime;
if (this._maximumRecordTime === -1 || endTime > this._maximumRecordTime)
this._maximumRecordTime = endTime;
},
/**
* @param {Object} rawRecord
*/
recordOffsetInSeconds: function(rawRecord)
{
return WebInspector.TimelineModel.startTimeInSeconds(rawRecord) - this._minimumRecordTime;
},
__proto__: WebInspector.Object.prototype
}
/**
* @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.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} */ (JSON.parse(json));
} catch (e) {
WebInspector.showErrorMessage("Malformed timeline data.");
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() { }
}
/**
* @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
*/
onError: function(reader, event)
{
this._progress.done();
this._model.reset();
switch (event.target.error.code) {
case FileError.NOT_FOUND_ERR:
WebInspector.showErrorMessage(WebInspector.UIString("File \"%s\" not found.", reader.fileName()));
break;
case FileError.NOT_READABLE_ERR:
WebInspector.showErrorMessage(WebInspector.UIString("File \"%s\" is not readable", reader.fileName()));
break;
case FileError.ABORT_ERR:
break;
default:
WebInspector.showErrorMessage(WebInspector.UIString("An error occurred while reading the file \"%s\"", reader.fileName()));
}
}
}
/**
* @constructor
*/
WebInspector.TimelineSaver = function(stream)
{
this._stream = stream;
}
WebInspector.TimelineSaver.prototype = {
/**
* @param {Array} records
* @param {string} version
*/
save: function(records, version)
{
this._records = records;
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._records.length) {
stream.close();
return;
}
data.push("");
}
while (this._recordIndex < this._records.length) {
var item = JSON.stringify(this._records[this._recordIndex]);
var itemLength = item.length + separator.length;
if (length + itemLength > WebInspector.TimelineModel.TransferChunkLengthBytes)
break;
length += itemLength;
data.push(item);
++this._recordIndex;
}
if (this._recordIndex === this._records.length)
data.push(data.pop() + "]");
stream.write(data.join(separator), this._writeNextChunk.bind(this));
}
}