blob: 81b5f914fb22585ee1901111910eeb3fa897861e [file] [log] [blame]
/*
* 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
*/
WebInspector.TracingModel = function()
{
this.reset();
}
/**
* @enum {string}
*/
WebInspector.TracingModel.Phase = {
Begin: "B",
End: "E",
Complete: "X",
Instant: "I",
AsyncBegin: "S",
AsyncStepInto: "T",
AsyncStepPast: "p",
AsyncEnd: "F",
FlowBegin: "s",
FlowStep: "t",
FlowEnd: "f",
Metadata: "M",
Counter: "C",
Sample: "P",
CreateObject: "N",
SnapshotObject: "O",
DeleteObject: "D"
};
WebInspector.TracingModel.MetadataEvent = {
ProcessSortIndex: "process_sort_index",
ProcessName: "process_name",
ThreadSortIndex: "thread_sort_index",
ThreadName: "thread_name"
}
WebInspector.TracingModel.DevToolsMetadataEventCategory = "disabled-by-default-devtools.timeline";
WebInspector.TracingModel.ConsoleEventCategory = "blink.console";
WebInspector.TracingModel.FrameLifecycleEventCategory = "cc,devtools";
WebInspector.TracingModel.DevToolsMetadataEvent = {
TracingStartedInPage: "TracingStartedInPage",
TracingSessionIdForWorker: "TracingSessionIdForWorker",
};
/**
* @param {string} phase
* @return {boolean}
*/
WebInspector.TracingModel.isAsyncPhase = function(phase)
{
return phase === WebInspector.TracingModel.Phase.AsyncBegin || phase === WebInspector.TracingModel.Phase.AsyncEnd ||
phase === WebInspector.TracingModel.Phase.AsyncStepInto || phase === WebInspector.TracingModel.Phase.AsyncStepPast;
}
WebInspector.TracingModel.prototype = {
/**
* @return {!Array.<!WebInspector.TracingModel.Event>}
*/
devtoolsPageMetadataEvents: function()
{
return this._devtoolsPageMetadataEvents;
},
/**
* @return {!Array.<!WebInspector.TracingModel.Event>}
*/
devtoolsWorkerMetadataEvents: function()
{
return this._devtoolsWorkerMetadataEvents;
},
/**
* @return {?string}
*/
sessionId: function()
{
return this._sessionId;
},
/**
* @param {!Array.<!WebInspector.TracingManager.EventPayload>} events
*/
setEventsForTest: function(events)
{
this.reset();
this.addEvents(events);
this.tracingComplete();
},
/**
* @param {!Array.<!WebInspector.TracingManager.EventPayload>} events
*/
addEvents: function(events)
{
for (var i = 0; i < events.length; ++i)
this._addEvent(events[i]);
},
tracingComplete: function()
{
this._processMetadataEvents();
for (var process in this._processById)
this._processById[process]._tracingComplete(this._maximumRecordTime);
this._backingStorage.finishWriting(function() {});
},
reset: function()
{
this._processById = {};
this._minimumRecordTime = 0;
this._maximumRecordTime = 0;
this._sessionId = null;
this._devtoolsPageMetadataEvents = [];
this._devtoolsWorkerMetadataEvents = [];
if (this._backingStorage)
this._backingStorage.remove();
this._backingStorage = new WebInspector.DeferredTempFile("tracing", String(Date.now()));
this._storageOffset = 0;
},
/**
* @param {!WebInspector.OutputStream} outputStream
* @param {!WebInspector.OutputStreamDelegate} delegate
*/
writeToStream: function(outputStream, delegate)
{
this._backingStorage.writeToOutputStream(outputStream, delegate);
},
/**
* @param {!WebInspector.TracingManager.EventPayload} payload
*/
_addEvent: function(payload)
{
var process = this._processById[payload.pid];
if (!process) {
process = new WebInspector.TracingModel.Process(payload.pid);
this._processById[payload.pid] = process;
}
var stringPayload = JSON.stringify(payload);
var startOffset = this._storageOffset;
if (startOffset) {
var recordDelimiter = ",\n";
stringPayload = recordDelimiter + stringPayload;
startOffset += recordDelimiter.length;
}
this._storageOffset += stringPayload.length;
this._backingStorage.write([stringPayload]);
if (payload.ph !== WebInspector.TracingModel.Phase.Metadata) {
var timestamp = payload.ts / 1000;
// We do allow records for unrelated threads to arrive out-of-order,
// so there's a chance we're getting records from the past.
if (timestamp && (!this._minimumRecordTime || timestamp < this._minimumRecordTime))
this._minimumRecordTime = timestamp;
var endTimeStamp = (payload.ts + (payload.dur || 0)) / 1000;
this._maximumRecordTime = Math.max(this._maximumRecordTime, endTimeStamp);
var event = process._addEvent(payload);
if (!event)
return;
event._setBackingStorage(this._backingStorage, startOffset, this._storageOffset);
if (event.name === WebInspector.TracingModel.DevToolsMetadataEvent.TracingStartedInPage &&
event.category === WebInspector.TracingModel.DevToolsMetadataEventCategory) {
this._devtoolsPageMetadataEvents.push(event);
}
if (event.name === WebInspector.TracingModel.DevToolsMetadataEvent.TracingSessionIdForWorker &&
event.category === WebInspector.TracingModel.DevToolsMetadataEventCategory) {
this._devtoolsWorkerMetadataEvents.push(event);
}
return;
}
switch (payload.name) {
case WebInspector.TracingModel.MetadataEvent.ProcessSortIndex:
process._setSortIndex(payload.args["sort_index"]);
break;
case WebInspector.TracingModel.MetadataEvent.ProcessName:
process._setName(payload.args["name"]);
break;
case WebInspector.TracingModel.MetadataEvent.ThreadSortIndex:
process.threadById(payload.tid)._setSortIndex(payload.args["sort_index"]);
break;
case WebInspector.TracingModel.MetadataEvent.ThreadName:
process.threadById(payload.tid)._setName(payload.args["name"]);
break;
}
},
_processMetadataEvents: function()
{
this._devtoolsPageMetadataEvents.sort(WebInspector.TracingModel.Event.compareStartTime);
if (!this._devtoolsPageMetadataEvents.length) {
WebInspector.console.error(WebInspector.TracingModel.DevToolsMetadataEvent.TracingStartedInPage + " event not found.");
return;
}
var sessionId = this._devtoolsPageMetadataEvents[0].args["sessionId"];
this._sessionId = sessionId;
var mismatchingIds = {};
function checkSessionId(event)
{
var args = event.args;
// FIXME: put sessionId into args["data"] for TracingStartedInPage event.
if (args["data"])
args = args["data"];
var id = args["sessionId"];
if (id === sessionId)
return true;
mismatchingIds[id] = true;
return false;
}
this._devtoolsPageMetadataEvents = this._devtoolsPageMetadataEvents.filter(checkSessionId);
this._devtoolsWorkerMetadataEvents = this._devtoolsWorkerMetadataEvents.filter(checkSessionId);
var idList = Object.keys(mismatchingIds);
if (idList.length)
WebInspector.console.error("Timeline recording was started in more than one page simulaniously. Session id mismatch: " + this._sessionId + " and " + idList + ".");
},
/**
* @return {number}
*/
minimumRecordTime: function()
{
return this._minimumRecordTime;
},
/**
* @return {number}
*/
maximumRecordTime: function()
{
return this._maximumRecordTime;
},
/**
* @return {!Array.<!WebInspector.TracingModel.Process>}
*/
sortedProcesses: function()
{
return WebInspector.TracingModel.NamedObject._sort(Object.values(this._processById));
}
}
/**
* @constructor
* @param {!WebInspector.TracingModel} tracingModel
*/
WebInspector.TracingModel.Loader = function(tracingModel)
{
this._tracingModel = tracingModel;
this._firstChunkReceived = false;
}
WebInspector.TracingModel.Loader.prototype = {
/**
* @param {!Array.<!WebInspector.TracingManager.EventPayload>} events
*/
loadNextChunk: function(events)
{
if (!this._firstChunkReceived) {
this._tracingModel.reset();
this._firstChunkReceived = true;
}
this._tracingModel.addEvents(events);
},
finish: function()
{
this._tracingModel.tracingComplete();
}
}
/**
* @constructor
* @param {string} category
* @param {string} name
* @param {string} phase
* @param {number} startTime
* @param {?WebInspector.TracingModel.Thread} thread
*/
WebInspector.TracingModel.Event = function(category, name, phase, startTime, thread)
{
this.category = category;
this.name = name;
this.phase = phase;
this.startTime = startTime;
this.thread = thread;
this.args = {};
/** @type {?string} */
this.warning = null;
/** @type {?WebInspector.TracingModel.Event} */
this.initiator = null;
/** @type {?Array.<!ConsoleAgent.CallFrame>} */
this.stackTrace = null;
/** @type {?Element} */
this.previewElement = null;
/** @type {?string} */
this.imageURL = null;
/** @type {number} */
this.backendNodeId = 0;
/** @type {number} */
this.selfTime = 0;
}
/**
* @param {!WebInspector.TracingManager.EventPayload} payload
* @param {?WebInspector.TracingModel.Thread} thread
* @return {!WebInspector.TracingModel.Event}
*/
WebInspector.TracingModel.Event.fromPayload = function(payload, thread)
{
var event = new WebInspector.TracingModel.Event(payload.cat, payload.name, payload.ph, payload.ts / 1000, thread);
if (payload.args)
event.addArgs(payload.args);
else
console.error("Missing mandatory event argument 'args' at " + payload.ts / 1000);
if (typeof payload.dur === "number")
event.setEndTime((payload.ts + payload.dur) / 1000);
if (payload.id)
event.id = payload.id;
return event;
}
WebInspector.TracingModel.Event.prototype = {
/**
* @param {number} endTime
*/
setEndTime: function(endTime)
{
if (endTime < this.startTime) {
console.assert(false, "Event out of order: " + this.name);
return;
}
this.endTime = endTime;
this.duration = endTime - this.startTime;
},
/**
* @param {!Object} args
*/
addArgs: function(args)
{
// Shallow copy args to avoid modifying original payload which may be saved to file.
for (var name in args) {
if (name in this.args)
console.error("Same argument name (" + name + ") is used for begin and end phases of " + this.name);
this.args[name] = args[name];
}
},
/**
* @param {!WebInspector.TracingManager.EventPayload} payload
*/
_complete: function(payload)
{
if (payload.args)
this.addArgs(payload.args);
else
console.error("Missing mandatory event argument 'args' at " + payload.ts / 1000);
this.setEndTime(payload.ts / 1000);
},
/**
* @param {!WebInspector.DeferredTempFile} backingFile
* @param {number} startOffset
* @param {number} endOffset
*/
_setBackingStorage: function(backingFile, startOffset, endOffset)
{
}
}
/**
* @param {!WebInspector.TracingModel.Event} a
* @param {!WebInspector.TracingModel.Event} b
* @return {number}
*/
WebInspector.TracingModel.Event.compareStartTime = function (a, b)
{
return a.startTime - b.startTime;
}
/**
* @param {!WebInspector.TracingModel.Event} a
* @param {!WebInspector.TracingModel.Event} b
* @return {number}
*/
WebInspector.TracingModel.Event.orderedCompareStartTime = function (a, b)
{
// Array.mergeOrdered coalesces objects if comparator returns 0.
// To change this behavior this comparator return -1 in the case events
// startTime's are equal, so both events got placed into the result array.
return a.startTime - b.startTime || -1;
}
/**
* @constructor
* @extends {WebInspector.TracingModel.Event}
* @param {string} category
* @param {string} name
* @param {number} startTime
* @param {?WebInspector.TracingModel.Thread} thread
*/
WebInspector.TracingModel.ObjectSnapshot = function(category, name, startTime, thread)
{
WebInspector.TracingModel.Event.call(this, category, name, WebInspector.TracingModel.Phase.SnapshotObject, startTime, thread);
}
/**
* @param {!WebInspector.TracingManager.EventPayload} payload
* @param {?WebInspector.TracingModel.Thread} thread
* @return {!WebInspector.TracingModel.ObjectSnapshot}
*/
WebInspector.TracingModel.ObjectSnapshot.fromPayload = function(payload, thread)
{
var snapshot = new WebInspector.TracingModel.ObjectSnapshot(payload.cat, payload.name, payload.ts / 1000, thread);
if (payload.id)
snapshot.id = payload.id;
if (!payload.args || !payload.args["snapshot"]) {
console.error("Missing mandatory 'snapshot' argument at " + payload.ts / 1000);
return snapshot;
}
if (payload.args)
snapshot.addArgs(payload.args);
return snapshot;
}
WebInspector.TracingModel.ObjectSnapshot.prototype = {
/**
* @param {function(?Object)} callback
*/
requestObject: function(callback)
{
var snapshot = this.args["snapshot"];
if (snapshot) {
callback(snapshot);
return;
}
this._file.readRange(this._startOffset, this._endOffset, onRead);
/**
* @param {?string} result
*/
function onRead(result)
{
if (!result) {
callback(null);
return;
}
var snapshot;
try {
var payload = JSON.parse(result);
snapshot = payload["args"]["snapshot"];
} catch (e) {
WebInspector.console.error("Malformed event data in backing storage");
}
callback(snapshot);
}
},
/**
* @param {!WebInspector.DeferredTempFile} backingFile
* @param {number} startOffset
* @param {number} endOffset
* @override
*/
_setBackingStorage: function(backingFile, startOffset, endOffset)
{
if (endOffset - startOffset < 10000)
return;
this._file = backingFile;
this._startOffset = startOffset;
this._endOffset = endOffset;
this.args = {};
},
__proto__: WebInspector.TracingModel.Event.prototype
}
/**
* @constructor
*/
WebInspector.TracingModel.NamedObject = function()
{
}
WebInspector.TracingModel.NamedObject.prototype =
{
/**
* @param {string} name
*/
_setName: function(name)
{
this._name = name;
},
/**
* @return {string}
*/
name: function()
{
return this._name;
},
/**
* @param {number} sortIndex
*/
_setSortIndex: function(sortIndex)
{
this._sortIndex = sortIndex;
},
}
/**
* @param {!Array.<!WebInspector.TracingModel.NamedObject>} array
*/
WebInspector.TracingModel.NamedObject._sort = function(array)
{
/**
* @param {!WebInspector.TracingModel.NamedObject} a
* @param {!WebInspector.TracingModel.NamedObject} b
*/
function comparator(a, b)
{
return a._sortIndex !== b._sortIndex ? a._sortIndex - b._sortIndex : a.name().localeCompare(b.name());
}
return array.sort(comparator);
}
/**
* @constructor
* @extends {WebInspector.TracingModel.NamedObject}
* @param {number} id
*/
WebInspector.TracingModel.Process = function(id)
{
WebInspector.TracingModel.NamedObject.call(this);
this._setName("Process " + id);
this._threads = {};
this._objects = {};
/** @type {!Array.<!WebInspector.TracingManager.EventPayload>} */
this._asyncEvents = [];
/** @type {!Object.<string, ?Array.<!WebInspector.TracingModel.Event>>} */
this._openAsyncEvents = [];
}
WebInspector.TracingModel.Process.prototype = {
/**
* @param {number} id
* @return {!WebInspector.TracingModel.Thread}
*/
threadById: function(id)
{
var thread = this._threads[id];
if (!thread) {
thread = new WebInspector.TracingModel.Thread(this, id);
this._threads[id] = thread;
}
return thread;
},
/**
* @param {!WebInspector.TracingManager.EventPayload} payload
* @return {?WebInspector.TracingModel.Event} event
*/
_addEvent: function(payload)
{
var phase = WebInspector.TracingModel.Phase;
// Build async event when we've got events from all threads, so we can sort them and process in the chronological order.
// However, also add individual async events to the thread flow, so we can easily display them on the same chart as
// other events, should we choose so.
if (WebInspector.TracingModel.isAsyncPhase(payload.ph))
this._asyncEvents.push(payload);
var event = this.threadById(payload.tid)._addEvent(payload);
if (event && payload.ph === phase.SnapshotObject)
this.objectsByName(event.name).push(event);
return event;
},
/**
* @param {!number} lastEventTime
*/
_tracingComplete: function(lastEventTime)
{
/**
* @param {!WebInspector.TracingManager.EventPayload} a
* @param {!WebInspector.TracingManager.EventPayload} b
*/
function comparePayloadTimestamp(a, b)
{
return a.ts - b.ts;
}
this._asyncEvents.sort(comparePayloadTimestamp).forEach(this._addAsyncEvent, this);
for (var key in this._openAsyncEvents) {
var steps = this._openAsyncEvents[key];
if (!steps)
continue;
var startEvent = steps[0];
var syntheticEndEvent = new WebInspector.TracingModel.Event(startEvent.category, startEvent.name, WebInspector.TracingModel.Phase.AsyncEnd, lastEventTime, startEvent.thread);
steps.push(syntheticEndEvent);
}
this._asyncEvents = [];
this._openAsyncEvents = [];
},
/**
* @param {!WebInspector.TracingManager.EventPayload} payload
*/
_addAsyncEvent: function(payload)
{
var phase = WebInspector.TracingModel.Phase;
var timestamp = payload.ts / 1000;
var key = payload.name + "." + payload.id;
var steps = this._openAsyncEvents[key];
var thread = this.threadById(payload.tid);
if (payload.ph === phase.AsyncBegin) {
if (steps) {
console.error("Event " + key + " at " + timestamp + " was already started at " + steps[0].startTime);
return;
}
steps = [WebInspector.TracingModel.Event.fromPayload(payload, thread)];
this._openAsyncEvents[key] = steps;
thread._addAsyncEventSteps(steps);
return;
}
if (!steps) {
console.error("Unexpected async event, phase " + payload.ph + " at " + timestamp);
return;
}
var newEvent = WebInspector.TracingModel.Event.fromPayload(payload, thread);
if (payload.ph === phase.AsyncEnd) {
steps.push(newEvent);
delete this._openAsyncEvents[key];
} else if (payload.ph === phase.AsyncStepInto || payload.ph === phase.AsyncStepPast) {
var lastPhase = steps.peekLast().phase;
if (lastPhase !== phase.AsyncBegin && lastPhase !== payload.ph) {
console.assert(false, "Async event step phase mismatch: " + lastPhase + " at " + steps.peekLast().startTime + " vs. " + payload.ph + " at " + timestamp);
return;
}
steps.push(newEvent);
} else {
console.assert(false, "Invalid async event phase");
}
},
/**
* @param {string} name
* @return {!Array.<!WebInspector.TracingModel.Event>}
*/
objectsByName: function(name)
{
var objects = this._objects[name];
if (!objects) {
objects = [];
this._objects[name] = objects;
}
return objects;
},
/**
* @return {!Array.<string>}
*/
sortedObjectNames: function()
{
return Object.keys(this._objects).sort();
},
/**
* @return {!Array.<!WebInspector.TracingModel.Thread>}
*/
sortedThreads: function()
{
return WebInspector.TracingModel.NamedObject._sort(Object.values(this._threads));
},
__proto__: WebInspector.TracingModel.NamedObject.prototype
}
/**
* @constructor
* @extends {WebInspector.TracingModel.NamedObject}
* @param {!WebInspector.TracingModel.Process} process
* @param {number} id
*/
WebInspector.TracingModel.Thread = function(process, id)
{
WebInspector.TracingModel.NamedObject.call(this);
this._process = process;
this._setName("Thread " + id);
this._events = [];
this._asyncEvents = [];
this._id = id;
this._stack = [];
}
WebInspector.TracingModel.Thread.prototype = {
/**
* @return {?WebInspector.Target}
*/
target: function()
{
//FIXME: correctly specify target
return WebInspector.targetManager.targets()[0];
},
/**
* @param {!WebInspector.TracingManager.EventPayload} payload
* @return {?WebInspector.TracingModel.Event} event
*/
_addEvent: function(payload)
{
var timestamp = payload.ts / 1000;
if (payload.ph === WebInspector.TracingModel.Phase.End) {
// Quietly ignore unbalanced close events, they're legit (we could have missed start one).
if (!this._stack.length)
return null;
var top = this._stack.pop();
if (top.name !== payload.name || top.category !== payload.cat)
console.error("B/E events mismatch at " + top.startTime + " (" + top.name + ") vs. " + timestamp + " (" + payload.name + ")");
else
top._complete(payload);
return null;
}
var event = payload.ph === WebInspector.TracingModel.Phase.SnapshotObject
? WebInspector.TracingModel.ObjectSnapshot.fromPayload(payload, this)
: WebInspector.TracingModel.Event.fromPayload(payload, this);
if (payload.ph === WebInspector.TracingModel.Phase.Begin)
this._stack.push(event);
if (this._events.length && this._events.peekLast().startTime > event.startTime)
console.assert(false, "Event is out of order: " + event.name);
this._events.push(event);
return event;
},
/**
* @param {!Array.<!WebInspector.TracingModel.Event>} eventSteps
*/
_addAsyncEventSteps: function(eventSteps)
{
this._asyncEvents.push(eventSteps);
},
/**
* @return {number}
*/
id: function()
{
return this._id;
},
/**
* @return {!WebInspector.TracingModel.Process}
*/
process: function()
{
return this._process;
},
/**
* @return {!Array.<!WebInspector.TracingModel.Event>}
*/
events: function()
{
return this._events;
},
/**
* @return {!Array.<!WebInspector.TracingModel.Event>}
*/
asyncEvents: function()
{
return this._asyncEvents;
},
__proto__: WebInspector.TracingModel.NamedObject.prototype
}