blob: bdc0c417435d1e4f8b06d346c7c7427f0c5dda04 [file] [log] [blame]
/*
* Copyright (C) 2013 Apple 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:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. 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.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS 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 APPLE INC. OR ITS 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.
*/
WebInspector.TimelineManager = function()
{
WebInspector.Object.call(this);
WebInspector.Frame.addEventListener(WebInspector.Frame.Event.ProvisionalLoadStarted, this._startAutoRecording, this);
WebInspector.Frame.addEventListener(WebInspector.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
WebInspector.Frame.addEventListener(WebInspector.Frame.Event.ResourceWasAdded, this._resourceWasAdded, this);
this._recording = new WebInspector.TimelineRecording;
this._recordingEnabled = false;
};
WebInspector.TimelineManager.Event = {
RecordingStarted: "timeline-manager-recording-started",
RecordingStopped: "timeline-manager-recording-stopped"
};
WebInspector.TimelineManager.MaximumAutoRecordDuration = 90000; // 90 seconds
WebInspector.TimelineManager.MaximumAutoRecordDurationAfterLoadEvent = 10000; // 10 seconds
WebInspector.TimelineManager.DeadTimeRequiredToStopAutoRecordingEarly = 2000; // 2 seconds
WebInspector.TimelineManager.prototype = {
constructor: WebInspector.TimelineManager,
// Public
get recording()
{
return this._recording;
},
get recordingEnabled()
{
return this._recordingEnabled;
},
startRecording: function()
{
if (this._recordingEnabled)
return;
this._recordingEnabled = true;
TimelineAgent.start();
this.dispatchEventToListeners(WebInspector.TimelineManager.Event.RecordingStarted);
},
stopRecording: function()
{
if (!this._recordingEnabled)
return;
if (this._stopRecordingTimeout) {
clearTimeout(this._stopRecordingTimeout);
delete this._stopRecordingTimeout;
}
if (this._deadTimeTimeout) {
clearTimeout(this._deadTimeTimeout);
delete this._deadTimeTimeout;
}
TimelineAgent.stop();
this._recordingEnabled = false;
this._autoRecordingMainResource = null;
this.dispatchEventToListeners(WebInspector.TimelineManager.Event.RecordingStopped);
},
eventRecorded: function(originalRecordPayload)
{
// Called from WebInspector.TimelineObserver.
if (!this._recordingEnabled)
return;
function processRecord(recordPayload, parentRecordPayload)
{
// Convert the timestamps to seconds to match the resource timestamps.
var startTime = recordPayload.startTime / 1000;
var endTime = recordPayload.endTime / 1000;
var callFrames = this._callFramesFromPayload(recordPayload.stackTrace);
var significantCallFrame = null;
if (callFrames) {
for (var i = 0; i < callFrames.length; ++i) {
if (callFrames[i].nativeCode)
continue;
significantCallFrame = callFrames[i];
break;
}
}
var sourceCodeLocation = significantCallFrame && significantCallFrame.sourceCodeLocation;
switch (recordPayload.type) {
case TimelineAgent.EventType.MarkLoad:
console.assert(isNaN(endTime));
var frame = WebInspector.frameResourceManager.frameForIdentifier(recordPayload.frameId);
console.assert(frame);
if (!frame)
break;
frame.markLoadEvent(startTime);
if (!frame.isMainFrame())
break;
var eventMarker = new WebInspector.TimelineEventMarker(startTime, WebInspector.TimelineEventMarker.Type.LoadEvent);
this._recording.addEventMarker(eventMarker);
this._stopAutoRecordingSoon();
break;
case TimelineAgent.EventType.MarkDOMContent:
console.assert(isNaN(endTime));
var frame = WebInspector.frameResourceManager.frameForIdentifier(recordPayload.frameId);
console.assert(frame);
if (!frame)
break;
frame.markDOMContentReadyEvent(startTime);
if (!frame.isMainFrame())
break;
var eventMarker = new WebInspector.TimelineEventMarker(startTime, WebInspector.TimelineEventMarker.Type.DOMContentEvent);
this._recording.addEventMarker(eventMarker);
break;
case TimelineAgent.EventType.ScheduleStyleRecalculation:
console.assert(isNaN(endTime));
// Pass the startTime as the endTime since this record type has no duration.
this._addRecord(new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.InvalidateStyles, startTime, startTime, callFrames, sourceCodeLocation));
break;
case TimelineAgent.EventType.RecalculateStyles:
this._addRecord(new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.RecalculateStyles, startTime, endTime, callFrames, sourceCodeLocation));
break;
case TimelineAgent.EventType.InvalidateLayout:
console.assert(isNaN(endTime));
// Pass the startTime as the endTime since this record type has no duration.
this._addRecord(new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.InvalidateLayout, startTime, startTime, callFrames, sourceCodeLocation));
break;
case TimelineAgent.EventType.Layout:
// COMPATIBILITY (iOS 6): Layout records did not contain area properties. This is not exposed via a quad "root".
var quad = recordPayload.data.root ? new WebInspector.Quad(recordPayload.data.root) : null;
if (quad)
this._addRecord(new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.Layout, startTime, endTime, callFrames, sourceCodeLocation, quad.points[0].x, quad.points[0].y, quad.width, quad.height, quad));
else
this._addRecord(new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.Layout, startTime, endTime, callFrames, sourceCodeLocation));
break;
case TimelineAgent.EventType.Paint:
// COMPATIBILITY (iOS 6): Paint records data contained x, y, width, height properties. This became a quad "clip".
var quad = recordPayload.data.clip ? new WebInspector.Quad(recordPayload.data.clip) : null;
if (quad)
this._addRecord(new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.Paint, startTime, endTime, callFrames, sourceCodeLocation, null, null, quad.width, quad.height, quad));
else
this._addRecord(new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.Paint, startTime, endTime, callFrames, sourceCodeLocation, recordPayload.data.x, recordPayload.data.y, recordPayload.data.width, recordPayload.data.height));
break;
case TimelineAgent.EventType.EvaluateScript:
if (!sourceCodeLocation) {
var mainFrame = WebInspector.frameResourceManager.mainFrame;
var scriptResource = mainFrame.url === recordPayload.data.url ? mainFrame.mainResource : mainFrame.resourceForURL(recordPayload.data.url, true);
if (scriptResource) {
// The lineNumber is 1-based, but we expect 0-based.
var lineNumber = recordPayload.data.lineNumber - 1;
// FIXME: No column number is provided.
sourceCodeLocation = scriptResource.createSourceCodeLocation(lineNumber, 0);
}
}
switch (parentRecordPayload && parentRecordPayload.type) {
case TimelineAgent.EventType.TimerFire:
this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.TimerFired, startTime, endTime, callFrames, sourceCodeLocation, parentRecordPayload.data.timerId));
break;
default:
this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.ScriptEvaluated, startTime, endTime, callFrames, sourceCodeLocation, null));
break;
}
break;
case TimelineAgent.EventType.TimeStamp:
var eventMarker = new WebInspector.TimelineEventMarker(startTime, WebInspector.TimelineEventMarker.Type.TimeStamp);
this._recording.addEventMarker(eventMarker);
break;
case TimelineAgent.EventType.FunctionCall:
// FunctionCall always happens as a child of another record, and since the FunctionCall record
// has useful info we just make the timeline record here (combining the data from both records).
if (!parentRecordPayload)
break;
if (!sourceCodeLocation) {
var mainFrame = WebInspector.frameResourceManager.mainFrame;
var scriptResource = mainFrame.url === recordPayload.data.scriptName ? mainFrame.mainResource : mainFrame.resourceForURL(recordPayload.data.scriptName, true);
if (scriptResource) {
// The lineNumber is 1-based, but we expect 0-based.
var lineNumber = recordPayload.data.scriptLine - 1;
// FIXME: No column number is provided.
sourceCodeLocation = scriptResource.createSourceCodeLocation(lineNumber, 0);
}
}
switch (parentRecordPayload.type) {
case TimelineAgent.EventType.TimerFire:
this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.TimerFired, startTime, endTime, callFrames, sourceCodeLocation, parentRecordPayload.data.timerId));
break;
case TimelineAgent.EventType.EventDispatch:
this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.EventDispatched, startTime, endTime, callFrames, sourceCodeLocation, parentRecordPayload.data.type));
break;
case TimelineAgent.EventType.XHRLoad:
this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.EventDispatched, startTime, endTime, callFrames, sourceCodeLocation, "load"));
break;
case TimelineAgent.EventType.XHRReadyStateChange:
this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.EventDispatched, startTime, endTime, callFrames, sourceCodeLocation, "readystatechange"));
break;
case TimelineAgent.EventType.FireAnimationFrame:
this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.AnimationFrameFired, startTime, endTime, callFrames, sourceCodeLocation, parentRecordPayload.data.id));
break;
}
break;
case TimelineAgent.EventType.TimerInstall:
console.assert(isNaN(endTime));
// Pass the startTime as the endTime since this record type has no duration.
this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.TimerInstalled, startTime, startTime, callFrames, sourceCodeLocation, recordPayload.data.timerId));
break;
case TimelineAgent.EventType.TimerRemove:
console.assert(isNaN(endTime));
// Pass the startTime as the endTime since this record type has no duration.
this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.TimerRemoved, startTime, startTime, callFrames, sourceCodeLocation, recordPayload.data.timerId));
break;
}
}
// Iterate over the records tree using a stack. Doing this recursively has
// been known to cause a call stack overflow. https://webkit.org/b/79106
var stack = [{array: [originalRecordPayload], parent: null, index: 0}];
while (stack.length) {
var entry = stack.lastValue;
var recordPayloads = entry.array;
var parentRecordPayload = entry.parent;
if (entry.index < recordPayloads.length) {
var recordPayload = recordPayloads[entry.index];
processRecord.call(this, recordPayload, parentRecordPayload);
if (recordPayload.children)
stack.push({array: recordPayload.children, parent: recordPayload, index: 0});
++entry.index;
} else
stack.pop();
}
},
pageDidLoad: function(timestamp)
{
if (isNaN(WebInspector.frameResourceManager.mainFrame.loadEventTimestamp))
WebInspector.frameResourceManager.mainFrame.markLoadEvent(timestamp);
},
// Private
_callFramesFromPayload: function(payload)
{
if (!payload)
return null;
function createCallFrame(payload)
{
var url = payload.url;
var nativeCode = false;
if (url === "[native code]") {
nativeCode = true;
url = null;
}
var sourceCode = WebInspector.frameResourceManager.resourceForURL(url);
if (!sourceCode)
sourceCode = WebInspector.debuggerManager.scriptsForURL(url)[0];
// The lineNumber is 1-based, but we expect 0-based.
var lineNumber = payload.lineNumber - 1;
var sourceCodeLocation = sourceCode ? sourceCode.createSourceCodeLocation(lineNumber, payload.columnNumber) : null;
var functionName = payload.functionName !== "global code" ? payload.functionName : null;
return new WebInspector.CallFrame(null, sourceCodeLocation, functionName, null, null, nativeCode);
}
return payload.map(createCallFrame);
},
_addRecord: function(record)
{
this._recording.addRecord(record);
// Only worry about dead time after the load event.
if (!isNaN(WebInspector.frameResourceManager.mainFrame.loadEventTimestamp))
this._resetAutoRecordingDeadTimeTimeout();
},
_startAutoRecording: function(event)
{
if (!event.target.isMainFrame() || (this._recordingEnabled && !this._autoRecordingMainResource))
return false;
var mainResource = event.target.provisionalMainResource || event.target.mainResource;
if (mainResource === this._autoRecordingMainResource)
return false;
this.stopRecording();
this._autoRecordingMainResource = mainResource;
this._recording.reset();
this.startRecording();
this._addRecord(new WebInspector.ResourceTimelineRecord(mainResource));
if (this._stopRecordingTimeout)
clearTimeout(this._stopRecordingTimeout);
this._stopRecordingTimeout = setTimeout(this.stopRecording.bind(this), WebInspector.TimelineManager.MaximumAutoRecordDuration);
return true;
},
_stopAutoRecordingSoon: function()
{
// Only auto stop when auto recording.
if (!this._recordingEnabled || !this._autoRecordingMainResource)
return;
if (this._stopRecordingTimeout)
clearTimeout(this._stopRecordingTimeout);
this._stopRecordingTimeout = setTimeout(this.stopRecording.bind(this), WebInspector.TimelineManager.MaximumAutoRecordDurationAfterLoadEvent);
},
_resetAutoRecordingDeadTimeTimeout: function()
{
// Only monitor dead time when auto recording.
if (!this._recordingEnabled || !this._autoRecordingMainResource)
return;
if (this._deadTimeTimeout)
clearTimeout(this._deadTimeTimeout);
this._deadTimeTimeout = setTimeout(this.stopRecording.bind(this), WebInspector.TimelineManager.DeadTimeRequiredToStopAutoRecordingEarly);
},
_mainResourceDidChange: function(event)
{
// Ignore resource events when there isn't a main frame yet. Those events are triggered by
// loading the cached resources when the inspector opens, and they do not have timing information.
if (!WebInspector.frameResourceManager.mainFrame)
return;
if (this._startAutoRecording(event))
return;
if (!this._recordingEnabled)
return;
var mainResource = event.target.mainResource;
if (mainResource === this._autoRecordingMainResource)
return;
this._addRecord(new WebInspector.ResourceTimelineRecord(mainResource));
},
_resourceWasAdded: function(event)
{
// Ignore resource events when there isn't a main frame yet. Those events are triggered by
// loading the cached resources when the inspector opens, and they do not have timing information.
if (!WebInspector.frameResourceManager.mainFrame)
return;
if (!this._recordingEnabled)
return;
this._addRecord(new WebInspector.ResourceTimelineRecord(event.data.resource));
}
};
WebInspector.TimelineManager.prototype.__proto__ = WebInspector.Object.prototype;