blob: a12138a9db3d64e5ef9e1270d08659243ac7c2e4 [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
* @param {!WebInspector.TracingModel} tracingModel
* @param {!WebInspector.TimelineModel.Filter} eventFilter
* @extends {WebInspector.Object}
* @implements {WebInspector.TargetManager.Observer}
* @implements {WebInspector.TracingManagerClient}
*/
WebInspector.TimelineModel = function(tracingModel, eventFilter)
{
WebInspector.Object.call(this);
this._filters = [];
this._tracingModel = tracingModel;
this._eventFilter = eventFilter;
this._targets = [];
this.reset();
WebInspector.targetManager.observeTargets(this);
}
/**
* @enum {string}
*/
WebInspector.TimelineModel.RecordType = {
Task: "Task",
Program: "Program",
EventDispatch: "EventDispatch",
GPUTask: "GPUTask",
Animation: "Animation",
RequestMainThreadFrame: "RequestMainThreadFrame",
BeginFrame: "BeginFrame",
NeedsBeginFrameChanged: "NeedsBeginFrameChanged",
BeginMainThreadFrame: "BeginMainThreadFrame",
ActivateLayerTree: "ActivateLayerTree",
DrawFrame: "DrawFrame",
HitTest: "HitTest",
ScheduleStyleRecalculation: "ScheduleStyleRecalculation",
RecalculateStyles: "RecalculateStyles", // For backwards compatibility only, now replaced by UpdateLayoutTree.
UpdateLayoutTree: "UpdateLayoutTree",
InvalidateLayout: "InvalidateLayout",
Layout: "Layout",
UpdateLayer: "UpdateLayer",
UpdateLayerTree: "UpdateLayerTree",
PaintSetup: "PaintSetup",
Paint: "Paint",
PaintImage: "PaintImage",
Rasterize: "Rasterize",
RasterTask: "RasterTask",
ScrollLayer: "ScrollLayer",
CompositeLayers: "CompositeLayers",
ScheduleStyleInvalidationTracking: "ScheduleStyleInvalidationTracking",
StyleRecalcInvalidationTracking: "StyleRecalcInvalidationTracking",
StyleInvalidatorInvalidationTracking: "StyleInvalidatorInvalidationTracking",
LayoutInvalidationTracking: "LayoutInvalidationTracking",
LayerInvalidationTracking: "LayerInvalidationTracking",
PaintInvalidationTracking: "PaintInvalidationTracking",
ScrollInvalidationTracking: "ScrollInvalidationTracking",
ParseHTML: "ParseHTML",
ParseAuthorStyleSheet: "ParseAuthorStyleSheet",
TimerInstall: "TimerInstall",
TimerRemove: "TimerRemove",
TimerFire: "TimerFire",
XHRReadyStateChange: "XHRReadyStateChange",
XHRLoad: "XHRLoad",
EvaluateScript: "EvaluateScript",
CommitLoad: "CommitLoad",
MarkLoad: "MarkLoad",
MarkDOMContent: "MarkDOMContent",
MarkFirstPaint: "MarkFirstPaint",
TimeStamp: "TimeStamp",
ConsoleTime: "ConsoleTime",
ResourceSendRequest: "ResourceSendRequest",
ResourceReceiveResponse: "ResourceReceiveResponse",
ResourceReceivedData: "ResourceReceivedData",
ResourceFinish: "ResourceFinish",
FunctionCall: "FunctionCall",
GCEvent: "GCEvent", // For backwards compatibility only, now replaced by MinorGC/MajorGC.
MajorGC: "MajorGC",
MinorGC: "MinorGC",
JSFrame: "JSFrame",
JSSample: "JSSample",
// V8Sample events are coming from tracing and contain raw stacks with function addresses.
// After being processed with help of JitCodeAdded and JitCodeMoved events they
// get translated into function infos and stored as stacks in JSSample events.
V8Sample: "V8Sample",
JitCodeAdded: "JitCodeAdded",
JitCodeMoved: "JitCodeMoved",
UpdateCounters: "UpdateCounters",
RequestAnimationFrame: "RequestAnimationFrame",
CancelAnimationFrame: "CancelAnimationFrame",
FireAnimationFrame: "FireAnimationFrame",
WebSocketCreate : "WebSocketCreate",
WebSocketSendHandshakeRequest : "WebSocketSendHandshakeRequest",
WebSocketReceiveHandshakeResponse : "WebSocketReceiveHandshakeResponse",
WebSocketDestroy : "WebSocketDestroy",
EmbedderCallback : "EmbedderCallback",
SetLayerTreeId: "SetLayerTreeId",
TracingStartedInPage: "TracingStartedInPage",
TracingSessionIdForWorker: "TracingSessionIdForWorker",
DecodeImage: "Decode Image",
ResizeImage: "Resize Image",
DrawLazyPixelRef: "Draw LazyPixelRef",
DecodeLazyPixelRef: "Decode LazyPixelRef",
LazyPixelRef: "LazyPixelRef",
LayerTreeHostImplSnapshot: "cc::LayerTreeHostImpl",
PictureSnapshot: "cc::Picture",
DisplayItemListSnapshot: "cc::DisplayItemList",
// CpuProfile is a virtual event created on frontend to support
// serialization of CPU Profiles within tracing timeline data.
CpuProfile: "CpuProfile"
}
WebInspector.TimelineModel.Events = {
RecordsCleared: "RecordsCleared",
RecordingStarted: "RecordingStarted",
RecordingStopped: "RecordingStopped",
RecordFilterChanged: "RecordFilterChanged",
BufferUsage: "BufferUsage",
RetrieveEventsProgress: "RetrieveEventsProgress"
}
WebInspector.TimelineModel.MainThreadName = "main";
WebInspector.TimelineModel.WorkerThreadName = "DedicatedWorker Thread";
WebInspector.TimelineModel.RendererMainThreadName = "CrRendererMain";
/**
* @param {!Array.<!WebInspector.TracingModel.Event>} events
* @param {function(!WebInspector.TracingModel.Event)} onStartEvent
* @param {function(!WebInspector.TracingModel.Event)} onEndEvent
* @param {function(!WebInspector.TracingModel.Event,?WebInspector.TracingModel.Event)=} onInstantEvent
*/
WebInspector.TimelineModel.forEachEvent = function(events, onStartEvent, onEndEvent, onInstantEvent)
{
var stack = [];
for (var i = 0; i < events.length; ++i) {
var e = events[i];
if (WebInspector.TracingModel.isAsyncPhase(e.phase) || WebInspector.TracingModel.isFlowPhase(e.phase))
continue;
while (stack.length && stack.peekLast().endTime <= e.startTime)
onEndEvent(stack.pop());
if (e.duration) {
onStartEvent(e);
stack.push(e);
} else {
onInstantEvent && onInstantEvent(e, stack.peekLast() || null);
}
}
while (stack.length)
onEndEvent(stack.pop());
}
/**
* @param {!Array.<!WebInspector.TimelineModel.Record>} recordsArray
* @param {?function(!WebInspector.TimelineModel.Record)|?function(!WebInspector.TimelineModel.Record,number)} preOrderCallback
* @param {function(!WebInspector.TimelineModel.Record)|function(!WebInspector.TimelineModel.Record,number)=} postOrderCallback
* @return {boolean}
*/
WebInspector.TimelineModel.forAllRecords = function(recordsArray, preOrderCallback, postOrderCallback)
{
/**
* @param {!Array.<!WebInspector.TimelineModel.Record>} records
* @param {number} depth
* @return {boolean}
*/
function processRecords(records, depth)
{
for (var i = 0; i < records.length; ++i) {
var record = records[i];
if (preOrderCallback && preOrderCallback(record, depth))
return true;
if (processRecords(record.children(), depth + 1))
return true;
if (postOrderCallback && postOrderCallback(record, depth))
return true;
}
return false;
}
return processRecords(recordsArray, 0);
}
WebInspector.TimelineModel.TransferChunkLengthBytes = 5000000;
WebInspector.TimelineModel.DevToolsMetadataEvent = {
TracingStartedInPage: "TracingStartedInPage",
TracingSessionIdForWorker: "TracingSessionIdForWorker",
};
/**
* @constructor
* @param {string} name
*/
WebInspector.TimelineModel.VirtualThread = function(name)
{
this.name = name;
/** @type {!Array<!WebInspector.TracingModel.Event>} */
this.events = [];
/** @type {!Map<!WebInspector.AsyncEventGroup, !Array<!WebInspector.TracingModel.AsyncEvent>>} */
this.asyncEventsByGroup = new Map();
}
WebInspector.TimelineModel.VirtualThread.prototype = {
/**
* @return {boolean}
*/
isWorker: function()
{
return this.name === WebInspector.TimelineModel.WorkerThreadName;
}
}
/**
* @constructor
* @param {!WebInspector.TracingModel.Event} traceEvent
*/
WebInspector.TimelineModel.Record = function(traceEvent)
{
this._event = traceEvent;
this._children = [];
}
/**
* @param {!WebInspector.TimelineModel.Record} a
* @param {!WebInspector.TimelineModel.Record} b
* @return {number}
*/
WebInspector.TimelineModel.Record._compareStartTime = function(a, b)
{
// Never return 0 as otherwise equal records would be merged.
return a.startTime() <= b.startTime() ? -1 : 1;
}
WebInspector.TimelineModel.Record.prototype = {
/**
* @return {?WebInspector.Target}
*/
target: function()
{
var threadName = this._event.thread.name();
//FIXME: correctly specify target
return threadName === WebInspector.TimelineModel.RendererMainThreadName ? WebInspector.targetManager.targets()[0] || null : null;
},
/**
* @return {!Array.<!WebInspector.TimelineModel.Record>}
*/
children: function()
{
return this._children;
},
/**
* @return {number}
*/
startTime: function()
{
return this._event.startTime;
},
/**
* @return {number}
*/
endTime: function()
{
return this._event.endTime || this._event.startTime;
},
/**
* @return {string}
*/
thread: function()
{
if (this._event.thread.name() === WebInspector.TimelineModel.RendererMainThreadName)
return WebInspector.TimelineModel.MainThreadName;
return this._event.thread.name();
},
/**
* @return {!WebInspector.TimelineModel.RecordType}
*/
type: function()
{
return WebInspector.TimelineModel._eventType(this._event);
},
/**
* @param {string} key
* @return {?Object}
*/
getUserObject: function(key)
{
if (key === "TimelineUIUtils::preview-element")
return this._event.previewElement;
throw new Error("Unexpected key: " + key);
},
/**
* @param {string} key
* @param {?Object|undefined} value
*/
setUserObject: function(key, value)
{
if (key !== "TimelineUIUtils::preview-element")
throw new Error("Unexpected key: " + key);
this._event.previewElement = /** @type {?Element} */ (value);
},
/**
* @return {!WebInspector.TracingModel.Event}
*/
traceEvent: function()
{
return this._event;
},
/**
* @param {!WebInspector.TimelineModel.Record} child
*/
_addChild: function(child)
{
this._children.push(child);
child.parent = this;
}
}
/** @typedef {!{page: !Array<!WebInspector.TracingModel.Event>, workers: !Array<!WebInspector.TracingModel.Event>}} */
WebInspector.TimelineModel.MetadataEvents;
/**
* @return {!WebInspector.TimelineModel.RecordType}
*/
WebInspector.TimelineModel._eventType = function(event)
{
if (event.hasCategory(WebInspector.TracingModel.ConsoleEventCategory))
return WebInspector.TimelineModel.RecordType.ConsoleTime;
return /** @type !WebInspector.TimelineModel.RecordType */ (event.name);
}
WebInspector.TimelineModel.prototype = {
/**
* @param {boolean} captureCauses
* @param {boolean} enableJSSampling
* @param {boolean} captureMemory
* @param {boolean} capturePictures
* @param {boolean} captureFilmStrip
*/
startRecording: function(captureCauses, enableJSSampling, captureMemory, capturePictures, captureFilmStrip)
{
function disabledByDefault(category)
{
return "disabled-by-default-" + category;
}
var categoriesArray = [
"-*",
"devtools.timeline",
disabledByDefault("devtools.timeline"),
disabledByDefault("devtools.timeline.frame"),
WebInspector.TracingModel.TopLevelEventCategory,
WebInspector.TracingModel.ConsoleEventCategory
];
if (Runtime.experiments.isEnabled("timelineFlowEvents")) {
categoriesArray.push(disabledByDefault("toplevel.flow"),
disabledByDefault("ipc.flow"));
}
if (Runtime.experiments.isEnabled("timelineTracingJSProfile") && enableJSSampling) {
categoriesArray.push(disabledByDefault("v8.cpu_profile"));
if (WebInspector.moduleSetting("highResolutionCpuProfiling").get())
categoriesArray.push(disabledByDefault("v8.cpu_profile.hires"));
}
if (captureCauses || enableJSSampling)
categoriesArray.push(disabledByDefault("devtools.timeline.stack"));
if (captureCauses && Runtime.experiments.isEnabled("timelineInvalidationTracking"))
categoriesArray.push(disabledByDefault("devtools.timeline.invalidationTracking"));
if (capturePictures) {
categoriesArray.push(disabledByDefault("devtools.timeline.layers"),
disabledByDefault("devtools.timeline.picture"),
disabledByDefault("blink.graphics_context_annotations"));
}
if (captureFilmStrip)
categoriesArray.push(disabledByDefault("devtools.screenshot"));
var categories = categoriesArray.join(",");
this._startRecordingWithCategories(categories, enableJSSampling);
},
stopRecording: function()
{
WebInspector.targetManager.resumeAllTargets();
this._allProfilesStoppedPromise = this._stopProfilingOnAllTargets();
if (this._targets[0])
this._targets[0].tracingManager.stop();
},
/**
* @param {?function(!WebInspector.TimelineModel.Record)|?function(!WebInspector.TimelineModel.Record,number)} preOrderCallback
* @param {function(!WebInspector.TimelineModel.Record)|function(!WebInspector.TimelineModel.Record,number)=} postOrderCallback
*/
forAllRecords: function(preOrderCallback, postOrderCallback)
{
WebInspector.TimelineModel.forAllRecords(this._records, preOrderCallback, postOrderCallback);
},
/**
* @param {!WebInspector.TimelineModel.Filter} filter
*/
addFilter: function(filter)
{
this._filters.push(filter);
filter.addEventListener(WebInspector.TimelineModel.Filter.Events.Changed, this._filterChanged, this);
},
/**
* @param {function(!WebInspector.TimelineModel.Record)|function(!WebInspector.TimelineModel.Record,number)} callback
*/
forAllFilteredRecords: function(callback)
{
/**
* @param {!WebInspector.TimelineModel.Record} record
* @param {number} depth
* @this {WebInspector.TimelineModel}
* @return {boolean}
*/
function processRecord(record, depth)
{
var visible = this.isVisible(record.traceEvent());
if (visible) {
if (callback(record, depth))
return true;
}
for (var i = 0; i < record.children().length; ++i) {
if (processRecord.call(this, record.children()[i], visible ? depth + 1 : depth))
return true;
}
return false;
}
for (var i = 0; i < this._records.length; ++i)
processRecord.call(this, this._records[i], 0);
},
/**
* @param {!WebInspector.TracingModel.Event} event
* @return {boolean}
*/
isVisible: function(event)
{
for (var i = 0; i < this._filters.length; ++i) {
if (!this._filters[i].accept(event))
return false;
}
return true;
},
_filterChanged: function()
{
this.dispatchEventToListeners(WebInspector.TimelineModel.Events.RecordFilterChanged);
},
/**
* @return {!Array.<!WebInspector.TimelineModel.Record>}
*/
records: function()
{
return this._records;
},
/**
* @return {?string}
*/
sessionId: function()
{
return this._sessionId;
},
/**
* @return {?WebInspector.Target}
*/
target: function()
{
// FIXME: Consider returning null for loaded traces.
return this._targets[0];
},
/**
* @param {!Array.<!WebInspector.TracingManager.EventPayload>} events
*/
setEventsForTest: function(events)
{
this._startCollectingTraceEvents(false);
this._tracingModel.addEvents(events);
this.tracingComplete();
},
/**
* @override
* @param {!WebInspector.Target} target
*/
targetAdded: function(target)
{
this._targets.push(target);
if (this._profiling)
this._startProfilingOnTarget(target);
},
/**
* @override
* @param {!WebInspector.Target} target
*/
targetRemoved: function(target)
{
this._targets.remove(target, true);
// FIXME: We'd like to stop profiling on the target and retrieve a profile
// but it's too late. Backend connection is closed.
},
/**
* @param {!WebInspector.Target} target
* @return {!Promise}
*/
_startProfilingOnTarget: function(target)
{
return target.profilerAgent().start();
},
/**
* @return {!Promise}
*/
_startProfilingOnAllTargets: function()
{
var intervalUs = WebInspector.moduleSetting("highResolutionCpuProfiling").get() ? 100 : 1000;
this._targets[0].profilerAgent().setSamplingInterval(intervalUs);
this._profiling = true;
return Promise.all(this._targets.map(this._startProfilingOnTarget));
},
/**
* @param {!WebInspector.Target} target
* @return {!Promise}
*/
_stopProfilingOnTarget: function(target)
{
/**
* @param {?Protocol.Error} error
* @param {?ProfilerAgent.CPUProfile} profile
* @return {?ProfilerAgent.CPUProfile}
*/
function extractProfile(error, profile)
{
return !error && profile ? profile : null;
}
return target.profilerAgent().stop(extractProfile).then(this._addCpuProfile.bind(this, target.id()));
},
/**
* @return {!Promise}
*/
_stopProfilingOnAllTargets: function()
{
var targets = this._profiling ? this._targets : [];
this._profiling = false;
return Promise.all(targets.map(this._stopProfilingOnTarget, this));
},
/**
* @param {string} categories
* @param {boolean=} enableJSSampling
* @param {function(?string)=} callback
*/
_startRecordingWithCategories: function(categories, enableJSSampling, callback)
{
if (!this._targets.length)
return;
WebInspector.targetManager.suspendAllTargets();
var profilingStartedPromise = enableJSSampling && !Runtime.experiments.isEnabled("timelineTracingJSProfile") ?
this._startProfilingOnAllTargets() : Promise.resolve();
var samplingFrequencyHz = WebInspector.moduleSetting("highResolutionCpuProfiling").get() ? 10000 : 1000;
var options = "sampling-frequency=" + samplingFrequencyHz;
var mainTarget = this._targets[0];
var tracingManager = mainTarget.tracingManager;
mainTarget.resourceTreeModel.suspendReload();
profilingStartedPromise.then(tracingManager.start.bind(tracingManager, this, categories, options, onTraceStarted));
/**
* @param {?string} error
*/
function onTraceStarted(error)
{
mainTarget.resourceTreeModel.resumeReload();
if (callback)
callback(error);
}
},
/**
* @param {boolean} fromFile
*/
_startCollectingTraceEvents: function(fromFile)
{
this._tracingModel.reset();
this.reset();
this.dispatchEventToListeners(WebInspector.TimelineModel.Events.RecordingStarted, { fromFile: fromFile });
},
/**
* @override
*/
tracingStarted: function()
{
this._startCollectingTraceEvents(false);
},
/**
* @param {!Array.<!WebInspector.TracingManager.EventPayload>} events
* @override
*/
traceEventsCollected: function(events)
{
this._tracingModel.addEvents(events);
},
/**
* @override
*/
tracingComplete: function()
{
if (!this._allProfilesStoppedPromise) {
this._didStopRecordingTraceEvents();
return;
}
this._allProfilesStoppedPromise.then(this._didStopRecordingTraceEvents.bind(this));
this._allProfilesStoppedPromise = null;
},
/**
* @param {number} usage
* @override
*/
tracingBufferUsage: function(usage)
{
this.dispatchEventToListeners(WebInspector.TimelineModel.Events.BufferUsage, usage);
},
/**
* @param {number} progress
* @override
*/
eventsRetrievalProgress: function(progress)
{
this.dispatchEventToListeners(WebInspector.TimelineModel.Events.RetrieveEventsProgress, progress);
},
/**
* @param {number} targetId
* @param {?ProfilerAgent.CPUProfile} cpuProfile
*/
_addCpuProfile: function(targetId, cpuProfile)
{
if (!cpuProfile)
return;
if (!this._cpuProfiles)
this._cpuProfiles = new Map();
this._cpuProfiles.set(targetId, cpuProfile);
},
_didStopRecordingTraceEvents: function()
{
var metadataEvents = this._processMetadataEvents();
this._injectCpuProfileEvents(metadataEvents);
this._tracingModel.tracingComplete();
this._resetProcessingState();
var startTime = 0;
for (var i = 0, length = metadataEvents.page.length; i < length; i++) {
var metaEvent = metadataEvents.page[i];
var process = metaEvent.thread.process();
var endTime = i + 1 < length ? metadataEvents.page[i + 1].startTime : Infinity;
this._currentPage = metaEvent.args["data"] && metaEvent.args["data"]["page"];
for (var thread of process.sortedThreads()) {
if (thread.name() === WebInspector.TimelineModel.WorkerThreadName && !metadataEvents.workers.some(function(e) { return e.args["data"]["workerThreadId"] === thread.id(); }))
continue;
this._processThreadEvents(startTime, endTime, metaEvent.thread, thread);
}
startTime = endTime;
}
this._inspectedTargetEvents.sort(WebInspector.TracingModel.Event.compareStartTime);
this._cpuProfiles = null;
this._buildTimelineRecords();
this._buildGPUTasks();
this._insertFirstPaintEvent();
this._resetProcessingState();
this.dispatchEventToListeners(WebInspector.TimelineModel.Events.RecordingStopped);
},
/**
* @return {!WebInspector.TimelineModel.MetadataEvents}
*/
_processMetadataEvents: function()
{
var metadataEvents = this._tracingModel.devToolsMetadataEvents();
var pageDevToolsMetadataEvents = [];
var workersDevToolsMetadataEvents = [];
for (var event of metadataEvents) {
if (event.name === WebInspector.TimelineModel.DevToolsMetadataEvent.TracingStartedInPage)
pageDevToolsMetadataEvents.push(event);
else if (event.name === WebInspector.TimelineModel.DevToolsMetadataEvent.TracingSessionIdForWorker)
workersDevToolsMetadataEvents.push(event);
}
if (!pageDevToolsMetadataEvents.length) {
// The trace is probably coming not from DevTools. Make a mock Metadata event.
var pageMetaEvent = this._loadedFromFile ? this._makeMockPageMetadataEvent() : null;
if (!pageMetaEvent) {
console.error(WebInspector.TimelineModel.DevToolsMetadataEvent.TracingStartedInPage + " event not found.");
return {page: [], workers: []};
}
pageDevToolsMetadataEvents.push(pageMetaEvent);
}
var sessionId = pageDevToolsMetadataEvents[0].args["sessionId"] || pageDevToolsMetadataEvents[0].args["data"]["sessionId"];
this._sessionId = sessionId;
var mismatchingIds = new Set();
/**
* @param {!WebInspector.TracingModel.Event} event
* @return {boolean}
*/
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.add(id);
return false;
}
var result = {
page: pageDevToolsMetadataEvents.filter(checkSessionId).sort(WebInspector.TracingModel.Event.compareStartTime),
workers: workersDevToolsMetadataEvents.filter(checkSessionId).sort(WebInspector.TracingModel.Event.compareStartTime)
};
if (mismatchingIds.size)
WebInspector.console.error("Timeline recording was started in more than one page simultaneously. Session id mismatch: " + this._sessionId + " and " + mismatchingIds.valuesArray() + ".");
return result;
},
/**
* @return {?WebInspector.TracingModel.Event}
*/
_makeMockPageMetadataEvent: function()
{
var rendererMainThreadName = WebInspector.TimelineModel.RendererMainThreadName;
// FIXME: pick up the first renderer process for now.
var process = Object.values(this._tracingModel.sortedProcesses()).filter(function(p) { return p.threadByName(rendererMainThreadName); })[0];
var thread = process && process.threadByName(rendererMainThreadName);
if (!thread)
return null;
var pageMetaEvent = new WebInspector.TracingModel.Event(
WebInspector.TracingModel.DevToolsMetadataEventCategory,
WebInspector.TimelineModel.DevToolsMetadataEvent.TracingStartedInPage,
WebInspector.TracingModel.Phase.Metadata,
this._tracingModel.minimumRecordTime(), thread);
pageMetaEvent.addArgs({"data": {"sessionId": "mockSessionId"}});
return pageMetaEvent;
},
/**
* @param {number} pid
* @param {number} tid
* @param {?ProfilerAgent.CPUProfile} cpuProfile
*/
_injectCpuProfileEvent: function(pid, tid, cpuProfile)
{
if (!cpuProfile)
return;
var cpuProfileEvent = /** @type {!WebInspector.TracingManager.EventPayload} */ ({
cat: WebInspector.TracingModel.DevToolsMetadataEventCategory,
ph: WebInspector.TracingModel.Phase.Instant,
ts: this._tracingModel.maximumRecordTime() * 1000,
pid: pid,
tid: tid,
name: WebInspector.TimelineModel.RecordType.CpuProfile,
args: { data: { cpuProfile: cpuProfile } }
});
this._tracingModel.addEvents([cpuProfileEvent]);
},
/**
* @param {!WebInspector.TimelineModel.MetadataEvents} metadataEvents
*/
_injectCpuProfileEvents: function(metadataEvents)
{
if (!this._cpuProfiles)
return;
var mainMetaEvent = metadataEvents.page.peekLast();
if (!mainMetaEvent)
return;
var pid = mainMetaEvent.thread.process().id();
var mainTarget = this._targets[0];
var mainCpuProfile = this._cpuProfiles.get(mainTarget.id());
this._injectCpuProfileEvent(pid, mainMetaEvent.thread.id(), mainCpuProfile);
for (var metaEvent of metadataEvents.workers) {
var workerId = metaEvent.args["data"]["workerId"];
var target = mainTarget.workerManager ? mainTarget.workerManager.targetByWorkerId(workerId) : null;
if (!target)
continue;
var cpuProfile = this._cpuProfiles.get(target.id());
this._injectCpuProfileEvent(pid, metaEvent.args["data"]["workerThreadId"], cpuProfile);
}
this._cpuProfiles = null;
},
_insertFirstPaintEvent: function()
{
if (!this._firstCompositeLayers)
return;
// First Paint is actually a DrawFrame that happened after first CompositeLayers following last CommitLoadEvent.
var recordTypes = WebInspector.TimelineModel.RecordType;
var i = insertionIndexForObjectInListSortedByFunction(this._firstCompositeLayers, this._inspectedTargetEvents, WebInspector.TracingModel.Event.compareStartTime);
for (; i < this._inspectedTargetEvents.length && this._inspectedTargetEvents[i].name !== recordTypes.DrawFrame; ++i) { }
if (i >= this._inspectedTargetEvents.length)
return;
var drawFrameEvent = this._inspectedTargetEvents[i];
var firstPaintEvent = new WebInspector.TracingModel.Event(drawFrameEvent.categoriesString, recordTypes.MarkFirstPaint, WebInspector.TracingModel.Phase.Instant, drawFrameEvent.startTime, drawFrameEvent.thread);
this._mainThreadEvents.splice(insertionIndexForObjectInListSortedByFunction(firstPaintEvent, this._mainThreadEvents, WebInspector.TracingModel.Event.compareStartTime), 0, firstPaintEvent);
var firstPaintRecord = new WebInspector.TimelineModel.Record(firstPaintEvent);
this._eventDividerRecords.splice(insertionIndexForObjectInListSortedByFunction(firstPaintRecord, this._eventDividerRecords, WebInspector.TimelineModel.Record._compareStartTime), 0, firstPaintRecord);
},
_buildTimelineRecords: function()
{
var topLevelRecords = this._buildTimelineRecordsForThread(this.mainThreadEvents());
for (var i = 0; i < topLevelRecords.length; i++) {
var record = topLevelRecords[i];
if (WebInspector.TracingModel.isTopLevelEvent(record.traceEvent()))
this._mainThreadTasks.push(record);
}
/**
* @param {!WebInspector.TimelineModel.VirtualThread} virtualThread
* @this {!WebInspector.TimelineModel}
*/
function processVirtualThreadEvents(virtualThread)
{
var threadRecords = this._buildTimelineRecordsForThread(virtualThread.events);
topLevelRecords = topLevelRecords.mergeOrdered(threadRecords, WebInspector.TimelineModel.Record._compareStartTime);
}
this.virtualThreads().forEach(processVirtualThreadEvents.bind(this));
this._records = topLevelRecords;
},
_buildGPUTasks: function()
{
var gpuProcess = this._tracingModel.processByName("GPU Process");
if (!gpuProcess)
return;
var mainThread = gpuProcess.threadByName("CrGpuMain");
if (!mainThread)
return;
var events = mainThread.events();
var recordTypes = WebInspector.TimelineModel.RecordType;
for (var i = 0; i < events.length; ++i) {
if (events[i].name === recordTypes.GPUTask)
this._gpuTasks.push(new WebInspector.TimelineModel.Record(events[i]));
}
},
/**
* @param {!Array.<!WebInspector.TracingModel.Event>} threadEvents
* @return {!Array.<!WebInspector.TimelineModel.Record>}
*/
_buildTimelineRecordsForThread: function(threadEvents)
{
var recordStack = [];
var topLevelRecords = [];
for (var i = 0, size = threadEvents.length; i < size; ++i) {
var event = threadEvents[i];
for (var top = recordStack.peekLast(); top && top._event.endTime <= event.startTime; top = recordStack.peekLast())
recordStack.pop();
if (event.phase === WebInspector.TracingModel.Phase.AsyncEnd || event.phase === WebInspector.TracingModel.Phase.NestableAsyncEnd)
continue;
var parentRecord = recordStack.peekLast();
// Maintain the back-end logic of old timeline, skip console.time() / console.timeEnd() that are not properly nested.
if (WebInspector.TracingModel.isAsyncBeginPhase(event.phase) && parentRecord && event.endTime > parentRecord._event.endTime)
continue;
var record = new WebInspector.TimelineModel.Record(event);
if (WebInspector.TimelineUIUtils.isMarkerEvent(event))
this._eventDividerRecords.push(record);
if (!this._eventFilter.accept(event) && !WebInspector.TracingModel.isTopLevelEvent(event))
continue;
if (parentRecord)
parentRecord._addChild(record);
else
topLevelRecords.push(record);
if (event.endTime)
recordStack.push(record);
}
return topLevelRecords;
},
_resetProcessingState: function()
{
this._asyncEventTracker = new WebInspector.TimelineAsyncEventTracker();
this._invalidationTracker = new WebInspector.InvalidationTracker();
this._layoutInvalidate = {};
this._lastScheduleStyleRecalculation = {};
this._paintImageEventByPixelRefId = {};
this._lastPaintForLayer = {};
this._lastRecalculateStylesEvent = null;
this._currentScriptEvent = null;
this._eventStack = [];
this._hadCommitLoad = false;
this._firstCompositeLayers = null;
this._currentPage = null;
},
/**
* @param {number} startTime
* @param {?number} endTime
* @param {!WebInspector.TracingModel.Thread} mainThread
* @param {!WebInspector.TracingModel.Thread} thread
*/
_processThreadEvents: function(startTime, endTime, mainThread, thread)
{
var events = thread.events();
var asyncEvents = thread.asyncEvents();
var jsSamples;
if (Runtime.experiments.isEnabled("timelineTracingJSProfile")) {
jsSamples = WebInspector.TimelineJSProfileProcessor.processRawV8Samples(events);
} else {
var cpuProfileEvent = events.peekLast();
if (cpuProfileEvent && cpuProfileEvent.name === WebInspector.TimelineModel.RecordType.CpuProfile) {
var cpuProfile = cpuProfileEvent.args["data"]["cpuProfile"];
if (cpuProfile)
jsSamples = WebInspector.TimelineJSProfileProcessor.generateTracingEventsFromCpuProfile(cpuProfile, thread);
}
}
if (jsSamples) {
events = events.mergeOrdered(jsSamples, WebInspector.TracingModel.Event.orderedCompareStartTime);
var jsFrameEvents = WebInspector.TimelineJSProfileProcessor.generateJSFrameEvents(events);
events = jsFrameEvents.mergeOrdered(events, WebInspector.TracingModel.Event.orderedCompareStartTime);
}
var threadEvents;
var threadAsyncEventsByGroup;
if (thread === mainThread) {
threadEvents = this._mainThreadEvents;
threadAsyncEventsByGroup = this._mainThreadAsyncEventsByGroup;
} else {
var virtualThread = new WebInspector.TimelineModel.VirtualThread(thread.name());
this._virtualThreads.push(virtualThread);
threadEvents = virtualThread.events;
threadAsyncEventsByGroup = virtualThread.asyncEventsByGroup;
}
this._eventStack = [];
var i = events.lowerBound(startTime, function (time, event) { return time - event.startTime });
var length = events.length;
for (; i < length; i++) {
var event = events[i];
if (endTime && event.startTime >= endTime)
break;
if (!this._processEvent(event))
continue;
threadEvents.push(event);
this._inspectedTargetEvents.push(event);
}
i = asyncEvents.lowerBound(startTime, function (time, asyncEvent) { return time - asyncEvent.startTime });
for (; i < asyncEvents.length; ++i) {
var asyncEvent = asyncEvents[i];
if (endTime && asyncEvent.startTime >= endTime)
break;
var asyncGroup = this._processAsyncEvent(asyncEvent);
if (!asyncGroup)
continue;
var groupAsyncEvents = threadAsyncEventsByGroup.get(asyncGroup);
if (!groupAsyncEvents) {
groupAsyncEvents = [];
threadAsyncEventsByGroup.set(asyncGroup, groupAsyncEvents);
}
groupAsyncEvents.push(asyncEvent);
}
},
/**
* @param {!WebInspector.TracingModel.Event} event
* @return {boolean}
*/
_processEvent: function(event)
{
var eventStack = this._eventStack;
while (eventStack.length && eventStack.peekLast().endTime <= event.startTime)
eventStack.pop();
var recordTypes = WebInspector.TimelineModel.RecordType;
if (this._currentScriptEvent && event.startTime > this._currentScriptEvent.endTime)
this._currentScriptEvent = null;
var eventData = event.args["data"] || event.args["beginData"] || {};
if (eventData && eventData["stackTrace"])
event.stackTrace = eventData["stackTrace"];
if (eventStack.length && eventStack.peekLast().name === recordTypes.EventDispatch)
eventStack.peekLast().hasChildren = true;
this._asyncEventTracker.processEvent(event);
if (event.initiator && event.initiator.url)
event.url = event.initiator.url;
switch (event.name) {
case recordTypes.ScheduleStyleRecalculation:
this._lastScheduleStyleRecalculation[event.args["data"]["frame"]] = event;
break;
case recordTypes.UpdateLayoutTree:
case recordTypes.RecalculateStyles:
this._invalidationTracker.didRecalcStyle(event);
if (event.args["beginData"])
event.initiator = this._lastScheduleStyleRecalculation[event.args["beginData"]["frame"]];
this._lastRecalculateStylesEvent = event;
break;
case recordTypes.ScheduleStyleInvalidationTracking:
case recordTypes.StyleRecalcInvalidationTracking:
case recordTypes.StyleInvalidatorInvalidationTracking:
case recordTypes.LayoutInvalidationTracking:
case recordTypes.LayerInvalidationTracking:
case recordTypes.PaintInvalidationTracking:
case recordTypes.ScrollInvalidationTracking:
this._invalidationTracker.addInvalidation(new WebInspector.InvalidationTrackingEvent(event));
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 = event;
var frameId = event.args["data"]["frame"];
if (!this._layoutInvalidate[frameId] && this._lastRecalculateStylesEvent && this._lastRecalculateStylesEvent.endTime > event.startTime)
layoutInitator = this._lastRecalculateStylesEvent.initiator;
this._layoutInvalidate[frameId] = layoutInitator;
break;
case recordTypes.Layout:
this._invalidationTracker.didLayout(event);
var frameId = event.args["beginData"]["frame"];
event.initiator = this._layoutInvalidate[frameId];
// In case we have no closing Layout event, endData is not available.
if (event.args["endData"]) {
event.backendNodeId = event.args["endData"]["rootNode"];
event.highlightQuad = event.args["endData"]["root"];
}
this._layoutInvalidate[frameId] = null;
if (this._currentScriptEvent)
event.warning = WebInspector.UIString("Forced synchronous layout is a possible performance bottleneck.");
break;
case recordTypes.EvaluateScript:
case recordTypes.FunctionCall:
if (!this._currentScriptEvent)
this._currentScriptEvent = event;
break;
case recordTypes.SetLayerTreeId:
this._inspectedTargetLayerTreeId = event.args["layerTreeId"] || event.args["data"]["layerTreeId"];
break;
case recordTypes.Paint:
this._invalidationTracker.didPaint(event);
event.highlightQuad = event.args["data"]["clip"];
event.backendNodeId = event.args["data"]["nodeId"];
var layerUpdateEvent = this._findAncestorEvent(recordTypes.UpdateLayer);
if (!layerUpdateEvent || layerUpdateEvent.args["layerTreeId"] !== this._inspectedTargetLayerTreeId)
break;
// Only keep layer paint events, skip paints for subframes that get painted to the same layer as parent.
if (!event.args["data"]["layerId"])
break;
this._lastPaintForLayer[layerUpdateEvent.args["layerId"]] = event;
break;
case recordTypes.DisplayItemListSnapshot:
case recordTypes.PictureSnapshot:
var layerUpdateEvent = this._findAncestorEvent(recordTypes.UpdateLayer);
if (!layerUpdateEvent || layerUpdateEvent.args["layerTreeId"] !== this._inspectedTargetLayerTreeId)
break;
var paintEvent = this._lastPaintForLayer[layerUpdateEvent.args["layerId"]];
if (paintEvent)
paintEvent.picture = event;
break;
case recordTypes.ScrollLayer:
event.backendNodeId = event.args["data"]["nodeId"];
break;
case recordTypes.PaintImage:
event.backendNodeId = event.args["data"]["nodeId"];
event.url = event.args["data"]["url"];
break;
case recordTypes.DecodeImage:
case recordTypes.ResizeImage:
var paintImageEvent = this._findAncestorEvent(recordTypes.PaintImage);
if (!paintImageEvent) {
var decodeLazyPixelRefEvent = this._findAncestorEvent(recordTypes.DecodeLazyPixelRef);
paintImageEvent = decodeLazyPixelRefEvent && this._paintImageEventByPixelRefId[decodeLazyPixelRefEvent.args["LazyPixelRef"]];
}
if (!paintImageEvent)
break;
event.backendNodeId = paintImageEvent.backendNodeId;
event.url = paintImageEvent.url;
break;
case recordTypes.DrawLazyPixelRef:
var paintImageEvent = this._findAncestorEvent(recordTypes.PaintImage);
if (!paintImageEvent)
break;
this._paintImageEventByPixelRefId[event.args["LazyPixelRef"]] = paintImageEvent;
event.backendNodeId = paintImageEvent.backendNodeId;
event.url = paintImageEvent.url;
break;
case recordTypes.MarkDOMContent:
case recordTypes.MarkLoad:
var page = eventData["page"];
if (page && page !== this._currentPage)
return false;
break;
case recordTypes.CommitLoad:
var page = eventData["page"];
if (page && page !== this._currentPage)
return false;
if (!eventData["isMainFrame"])
break;
this._hadCommitLoad = true;
this._firstCompositeLayers = null;
break;
case recordTypes.CompositeLayers:
if (!this._firstCompositeLayers && this._hadCommitLoad)
this._firstCompositeLayers = event;
break;
case recordTypes.Animation:
// FIXME: bring back Animation events as we figure out a way to show them while not cluttering the UI.
return false;
}
if (WebInspector.TracingModel.isAsyncPhase(event.phase))
return true;
var duration = event.duration;
if (!duration)
return true;
if (eventStack.length) {
var parent = eventStack.peekLast();
parent.selfTime -= duration;
if (parent.selfTime < 0) {
var epsilon = 1e-3;
if (parent.selfTime < -epsilon)
console.error("Children are longer than parent at " + event.startTime + " (" + (event.startTime - this.minimumRecordTime()).toFixed(3) + ") by " + parent.selfTime.toFixed(3));
parent.selfTime = 0;
}
}
event.selfTime = duration;
eventStack.push(event);
return true;
},
/**
* @param {!WebInspector.TracingModel.AsyncEvent} asyncEvent
* @return {?WebInspector.AsyncEventGroup}
*/
_processAsyncEvent: function(asyncEvent)
{
var groups = WebInspector.TimelineUIUtils.asyncEventGroups();
if (asyncEvent.hasCategory(WebInspector.TracingModel.ConsoleEventCategory))
return groups.console;
return null;
},
/**
* @param {string} name
* @return {?WebInspector.TracingModel.Event}
*/
_findAncestorEvent: function(name)
{
for (var i = this._eventStack.length - 1; i >= 0; --i) {
var event = this._eventStack[i];
if (event.name === name)
return event;
}
return null;
},
/**
* @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.TracingModelLoader(this, new WebInspector.ProgressProxy(null), fileReader.cancel.bind(fileReader));
fileReader.start(loader);
},
/**
* @param {string} url
* @param {!WebInspector.Progress} progress
*/
loadFromURL: function(url, progress)
{
var stream = new WebInspector.TracingModelLoader(this, progress);
WebInspector.ResourceLoader.loadAsStream(url, null, stream);
},
_createFileReader: function(file, delegate)
{
return new WebInspector.ChunkedFileReader(file, WebInspector.TimelineModel.TransferChunkLengthBytes, delegate);
},
reset: function()
{
this._virtualThreads = [];
/** @type {!Array.<!WebInspector.TracingModel.Event>} */
this._mainThreadEvents = [];
/** @type {!Map<!WebInspector.AsyncEventGroup, !Array<!WebInspector.TracingModel.AsyncEvent>>} */
this._mainThreadAsyncEventsByGroup = new Map();
/** @type {!Array.<!WebInspector.TracingModel.Event>} */
this._inspectedTargetEvents = [];
/** @type {!Array.<!WebInspector.TimelineModel.Record>} */
this._records = [];
/** @type {!Array.<!WebInspector.TimelineModel.Record>} */
this._mainThreadTasks = [];
/** @type {!Array.<!WebInspector.TimelineModel.Record>} */
this._gpuTasks = [];
/** @type {!Array.<!WebInspector.TimelineModel.Record>} */
this._eventDividerRecords = [];
/** @type {?string} */
this._sessionId = null;
this._loadedFromFile = false;
this.dispatchEventToListeners(WebInspector.TimelineModel.Events.RecordsCleared);
},
/**
* @return {number}
*/
minimumRecordTime: function()
{
return this._tracingModel.minimumRecordTime();
},
/**
* @return {number}
*/
maximumRecordTime: function()
{
return this._tracingModel.maximumRecordTime();
},
/**
* @return {!Array.<!WebInspector.TracingModel.Event>}
*/
inspectedTargetEvents: function()
{
return this._inspectedTargetEvents;
},
/**
* @return {!Array.<!WebInspector.TracingModel.Event>}
*/
mainThreadEvents: function()
{
return this._mainThreadEvents;
},
/**
* @param {!Array.<!WebInspector.TracingModel.Event>} events
*/
_setMainThreadEvents: function(events)
{
this._mainThreadEvents = events;
},
/**
* @return {!Map<!WebInspector.AsyncEventGroup, !Array.<!WebInspector.TracingModel.AsyncEvent>>}
*/
mainThreadAsyncEvents: function()
{
return this._mainThreadAsyncEventsByGroup;
},
/**
* @return {!Array.<!WebInspector.TimelineModel.VirtualThread>}
*/
virtualThreads: function()
{
return this._virtualThreads;
},
/**
* @return {boolean}
*/
isEmpty: function()
{
return this.minimumRecordTime() === 0 && this.maximumRecordTime() === 0;
},
/**
* @return {!Array.<!WebInspector.TimelineModel.Record>}
*/
mainThreadTasks: function()
{
return this._mainThreadTasks;
},
/**
* @return {!Array.<!WebInspector.TimelineModel.Record>}
*/
gpuTasks: function()
{
return this._gpuTasks;
},
/**
* @return {!Array.<!WebInspector.TimelineModel.Record>}
*/
eventDividerRecords: function()
{
return this._eventDividerRecords;
},
/**
* @return {!Array<!WebInspector.TimelineModel.NetworkRequest>}
*/
networkRequests: function()
{
/** @type {!Map<string,!WebInspector.TimelineModel.NetworkRequest>} */
var requests = new Map();
/** @type {!Array<!WebInspector.TimelineModel.NetworkRequest>} */
var requestsList = [];
/** @type {!Array<!WebInspector.TimelineModel.NetworkRequest>} */
var zeroStartRequestsList = [];
var types = WebInspector.TimelineModel.RecordType;
var resourceTypes = new Set([
types.ResourceSendRequest,
types.ResourceReceiveResponse,
types.ResourceReceivedData,
types.ResourceFinish
]);
var events = this.mainThreadEvents();
for (var i = 0; i < events.length; ++i) {
var e = events[i];
if (!resourceTypes.has(e.name))
continue;
var id = e.args["data"]["requestId"];
var request = requests.get(id);
if (request) {
request.addEvent(e);
} else {
request = new WebInspector.TimelineModel.NetworkRequest(e);
requests.set(id, request);
if (request.startTime)
requestsList.push(request);
else
zeroStartRequestsList.push(request);
}
}
return zeroStartRequestsList.concat(requestsList);
},
__proto__: WebInspector.Object.prototype
}
/**
* @constructor
*/
WebInspector.TimelineModel.ProfileTreeNode = function()
{
/** @type {number} */
this.totalTime;
/** @type {number} */
this.selfTime;
/** @type {string} */
this.name;
/** @type {string} */
this.color;
/** @type {string} */
this.id;
/** @type {!WebInspector.TracingModel.Event} */
this.event;
/** @type {?Map<string,!WebInspector.TimelineModel.ProfileTreeNode>} */
this.children;
/** @type {?WebInspector.TimelineModel.ProfileTreeNode} */
this.parent;
}
/**
* @param {!Array<!WebInspector.TracingModel.Event>} events
* @param {number} startTime
* @param {number} endTime
* @param {!Array<!WebInspector.TimelineModel.Filter>} filters
* @param {function(!WebInspector.TracingModel.Event):string} eventIdCallback
* @return {!WebInspector.TimelineModel.ProfileTreeNode}
*/
WebInspector.TimelineModel.buildTopDownTree = function(events, startTime, endTime, filters, eventIdCallback)
{
// Temporarily deposit a big enough value that exceeds the max recording time.
var /** @const */ initialTime = 1e7;
var root = new WebInspector.TimelineModel.ProfileTreeNode();
root.totalTime = initialTime;
root.selfTime = initialTime;
root.name = WebInspector.UIString("Top-Down Chart");
var parent = root;
/**
* @param {!WebInspector.TracingModel.Event} e
* @return {boolean}
*/
function filter(e)
{
if (!e.endTime && e.phase !== WebInspector.TracingModel.Phase.Instant)
return false;
if (e.endTime <= startTime || e.startTime >= endTime)
return false;
if (WebInspector.TracingModel.isAsyncPhase(e.phase))
return false;
for (var i = 0, l = filters.length; i < l; ++i) {
if (!filters[i].accept(e))
return false;
}
return true;
}
/**
* @param {!WebInspector.TracingModel.Event} e
*/
function onStartEvent(e)
{
if (!filter(e))
return;
var time = Math.min(endTime, e.endTime) - Math.max(startTime, e.startTime);
var id = eventIdCallback(e);
if (!parent.children)
parent.children = /** @type {!Map<string,!WebInspector.TimelineModel.ProfileTreeNode>} */ (new Map());
var node = parent.children.get(id);
if (node) {
node.selfTime += time;
node.totalTime += time;
} else {
node = new WebInspector.TimelineModel.ProfileTreeNode();
node.totalTime = time;
node.selfTime = time;
node.parent = parent;
node.id = id;
node.event = e;
parent.children.set(id, node);
}
parent.selfTime -= time;
if (parent.selfTime < 0) {
console.log("Error: Negative self of " + parent.selfTime, e);
parent.selfTime = 0;
}
parent = node;
}
/**
* @param {!WebInspector.TracingModel.Event} e
*/
function onEndEvent(e)
{
if (!filter(e))
return;
parent = parent.parent;
}
WebInspector.TimelineModel.forEachEvent(events, onStartEvent, onEndEvent);
root.totalTime -= root.selfTime;
root.selfTime = 0;
return root;
}
/**
* @param {!WebInspector.TimelineModel.ProfileTreeNode} topDownTree
* @param {?function(!WebInspector.TimelineModel.ProfileTreeNode):!WebInspector.TimelineModel.ProfileTreeNode=} groupingCallback
* @return {!WebInspector.TimelineModel.ProfileTreeNode}
*/
WebInspector.TimelineModel.buildBottomUpTree = function(topDownTree, groupingCallback)
{
var buRoot = new WebInspector.TimelineModel.ProfileTreeNode();
buRoot.selfTime = 0;
buRoot.totalTime = 0;
buRoot.name = WebInspector.UIString("Bottom-Up Chart");
/** @type {!Map<string,!WebInspector.TimelineModel.ProfileTreeNode>} */
buRoot.children = new Map();
var nodesOnStack = /** @type {!Set<string>} */ (new Set());
if (topDownTree.children)
topDownTree.children.forEach(processNode);
/**
* @param {!WebInspector.TimelineModel.ProfileTreeNode} tdNode
*/
function processNode(tdNode)
{
var buParent = groupingCallback && groupingCallback(tdNode) || buRoot;
appendNode(tdNode, buParent);
var hadNode = nodesOnStack.has(tdNode.id);
if (!hadNode)
nodesOnStack.add(tdNode.id);
if (tdNode.children)
tdNode.children.forEach(processNode);
if (!hadNode)
nodesOnStack.delete(tdNode.id);
}
/**
* @param {!WebInspector.TimelineModel.ProfileTreeNode} tdNode
* @param {!WebInspector.TimelineModel.ProfileTreeNode} buParent
*/
function appendNode(tdNode, buParent)
{
var selfTime = tdNode.selfTime;
var totalTime = tdNode.totalTime;
buParent.selfTime += selfTime;
buParent.totalTime += selfTime;
while (tdNode.parent) {
if (!buParent.children)
buParent.children = /** @type {!Map<string,!WebInspector.TimelineModel.ProfileTreeNode>} */ (new Map());
var id = tdNode.id;
var buNode = buParent.children.get(id);
if (!buNode) {
buNode = new WebInspector.TimelineModel.ProfileTreeNode();
buNode.selfTime = selfTime;
buNode.totalTime = totalTime;
buNode.name = tdNode.name;
buNode.event = tdNode.event;
buNode.id = id;
buParent.children.set(id, buNode);
} else {
buNode.selfTime += selfTime;
if (!nodesOnStack.has(id))
buNode.totalTime += totalTime;
}
tdNode = tdNode.parent;
buParent = buNode;
}
}
// Purge zero self time nodes.
var rootChildren = buRoot.children;
for (var item of rootChildren.entries()) {
if (item[1].selfTime === 0)
rootChildren.delete(item[0]);
}
return buRoot;
}
/**
* @constructor
* @param {!WebInspector.TracingModel.Event} event
*/
WebInspector.TimelineModel.NetworkRequest = function(event)
{
this.startTime = event.name === WebInspector.TimelineModel.RecordType.ResourceSendRequest ? event.startTime : 0;
this.endTime = Infinity;
this.addEvent(event);
}
WebInspector.TimelineModel.NetworkRequest.prototype = {
/**
* @param {!WebInspector.TracingModel.Event} event
*/
addEvent: function(event)
{
var recordType = WebInspector.TimelineModel.RecordType;
this.startTime = Math.min(this.startTime, event.startTime);
var eventData = event.args["data"];
if (eventData["mimeType"])
this.mimeType = eventData["mimeType"];
if (event.name === recordType.ResourceFinish)
this.endTime = event.startTime;
if (!this.responseTime && (event.name === recordType.ResourceReceiveResponse || event.name === recordType.ResourceReceivedData))
this.responseTime = event.startTime;
if (!this.url)
this.url = eventData["url"];
if (!this.requestMethod)
this.requestMethod = eventData["requestMethod"];
}
}
/**
* @constructor
* @extends {WebInspector.Object}
*/
WebInspector.TimelineModel.Filter = function()
{
WebInspector.Object.call(this);
}
WebInspector.TimelineModel.Filter.Events = {
Changed: "Changed"
}
WebInspector.TimelineModel.Filter.prototype = {
/**
* @param {!WebInspector.TracingModel.Event} event
* @return {boolean}
*/
accept: function(event)
{
return true;
},
notifyFilterChanged: function()
{
this.dispatchEventToListeners(WebInspector.TimelineModel.Filter.Events.Changed, this);
},
__proto__: WebInspector.Object.prototype
}
/**
* @constructor
* @extends {WebInspector.TimelineModel.Filter}
* @param {!Array.<string>} visibleTypes
*/
WebInspector.TimelineVisibleEventsFilter = function(visibleTypes)
{
WebInspector.TimelineModel.Filter.call(this);
this._visibleTypes = new Set(visibleTypes);
}
WebInspector.TimelineVisibleEventsFilter.prototype = {
/**
* @override
* @param {!WebInspector.TracingModel.Event} event
* @return {boolean}
*/
accept: function(event)
{
return this._visibleTypes.has(WebInspector.TimelineModel._eventType(event));
},
__proto__: WebInspector.TimelineModel.Filter.prototype
}
/**
* @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 = {
/**
* @override
*/
onTransferStarted: function()
{
this._progress.setTitle(WebInspector.UIString("Loading\u2026"));
},
/**
* @override
* @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());
}
},
/**
* @override
*/
onTransferFinished: function()
{
this._progress.done();
},
/**
* @override
* @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.console.error(WebInspector.UIString("File \"%s\" not found.", reader.fileName()));
break;
case FileError.NOT_READABLE_ERR:
WebInspector.console.error(WebInspector.UIString("File \"%s\" is not readable", reader.fileName()));
break;
case FileError.ABORT_ERR:
break;
default:
WebInspector.console.error(WebInspector.UIString("An error occurred while reading the file \"%s\"", reader.fileName()));
}
}
}
/**
* @constructor
* @extends {WebInspector.TimelineModel.Filter}
* @param {!Array<string>} excludeNames
*/
WebInspector.ExclusiveNameFilter = function(excludeNames)
{
WebInspector.TimelineModel.Filter.call(this);
this._excludeNames = new Set(excludeNames);
}
WebInspector.ExclusiveNameFilter.prototype = {
/**
* @override
* @param {!WebInspector.TracingModel.Event} event
* @return {boolean}
*/
accept: function(event)
{
return !this._excludeNames.has(event.name);
},
__proto__: WebInspector.TimelineModel.Filter.prototype
}
/**
* @constructor
* @extends {WebInspector.TimelineModel.Filter}
*/
WebInspector.ExcludeTopLevelFilter = function()
{
WebInspector.TimelineModel.Filter.call(this);
}
WebInspector.ExcludeTopLevelFilter.prototype = {
/**
* @override
* @param {!WebInspector.TracingModel.Event} event
* @return {boolean}
*/
accept: function(event)
{
return !WebInspector.TracingModel.isTopLevelEvent(event);
},
__proto__: WebInspector.TimelineModel.Filter.prototype
}
/**
* @constructor
* @implements {WebInspector.OutputStream}
* @param {!WebInspector.TimelineModel} model
* @param {!WebInspector.Progress} progress
* @param {function()=} canceledCallback
*/
WebInspector.TracingModelLoader = function(model, progress, canceledCallback)
{
this._model = model;
this._canceledCallback = canceledCallback;
this._progress = progress;
this._progress.setTitle(WebInspector.UIString("Loading"));
this._progress.setTotalWork(WebInspector.TracingModelLoader._totalProgress); // Unknown, will loop the values.
this._state = WebInspector.TracingModelLoader.State.Initial;
this._buffer = "";
this._firstChunk = true;
this._wasCanceledOnce = false;
this._loadedBytes = 0;
this._jsonTokenizer = new WebInspector.TextUtils.BalancedJSONTokenizer(this._writeBalancedJSON.bind(this), true);
}
WebInspector.TracingModelLoader._totalProgress = 100000;
WebInspector.TracingModelLoader.State = {
Initial: "Initial",
LookingForEvents: "LookingForEvents",
ReadingEvents: "ReadingEvents"
}
WebInspector.TracingModelLoader.prototype = {
/**
* @override
* @param {string} chunk
*/
write: function(chunk)
{
this._loadedBytes += chunk.length;
if (this._progress.isCanceled() && !this._wasCanceledOnce) {
this._wasCanceled = true;
this._reportErrorAndCancelLoading();
return;
}
this._progress.setWorked(this._loadedBytes % WebInspector.TracingModelLoader._totalProgress,
WebInspector.UIString("Loaded %s", Number.bytesToString(this._loadedBytes)));
if (this._state === WebInspector.TracingModelLoader.State.Initial) {
if (chunk[0] === "{")
this._state = WebInspector.TracingModelLoader.State.LookingForEvents;
else if (chunk[0] === "[")
this._state = WebInspector.TracingModelLoader.State.ReadingEvents;
else {
this._reportErrorAndCancelLoading(WebInspector.UIString("Malformed timeline data: Unknown JSON format"));
return;
}
}
if (this._state === WebInspector.TracingModelLoader.State.LookingForEvents) {
var objectName = "\"traceEvents\":";
var startPos = this._buffer.length - objectName.length;
this._buffer += chunk;
var pos = this._buffer.indexOf(objectName, startPos);
if (pos === -1)
return;
chunk = this._buffer.slice(pos + objectName.length)
this._state = WebInspector.TracingModelLoader.State.ReadingEvents;
}
this._jsonTokenizer.write(chunk);
},
/**
* @param {string} data
*/
_writeBalancedJSON: function(data)
{
var json = data + "]";
if (this._firstChunk) {
this._model._startCollectingTraceEvents(true);
} else {
var commaIndex = json.indexOf(",");
if (commaIndex !== -1)
json = json.slice(commaIndex + 1);
json = "[" + json;
}
var items;
try {
items = /** @type {!Array.<!WebInspector.TracingManager.EventPayload>} */ (JSON.parse(json));
} catch (e) {
this._reportErrorAndCancelLoading(WebInspector.UIString("Malformed timeline data: %s", e.toString()));
return;
}
if (this._firstChunk) {
this._firstChunk = false;
if (this._looksLikeAppVersion(items[0])) {
this._reportErrorAndCancelLoading(WebInspector.UIString("Legacy Timeline format is not supported."));
return;
}
}
try {
this._model._tracingModel.addEvents(items);
} catch(e) {
this._reportErrorAndCancelLoading(WebInspector.UIString("Malformed timeline data: %s", e.toString()));
return;
}
},
/**
* @param {string=} message
*/
_reportErrorAndCancelLoading: function(message)
{
if (message)
WebInspector.console.error(message);
this._model.tracingComplete();
this._model.reset();
if (this._canceledCallback)
this._canceledCallback();
this._progress.done();
},
_looksLikeAppVersion: function(item)
{
return typeof item === "string" && item.indexOf("Chrome") !== -1;
},
/**
* @override
*/
close: function()
{
this._model._loadedFromFile = true;
this._model.tracingComplete();
if (this._progress)
this._progress.done();
}
}
/**
* @constructor
* @param {!WebInspector.OutputStream} stream
* @implements {WebInspector.OutputStreamDelegate}
*/
WebInspector.TracingTimelineSaver = function(stream)
{
this._stream = stream;
}
WebInspector.TracingTimelineSaver.prototype = {
/**
* @override
*/
onTransferStarted: function()
{
this._stream.write("[");
},
/**
* @override
*/
onTransferFinished: function()
{
this._stream.write("]");
},
/**
* @override
* @param {!WebInspector.ChunkedReader} reader
*/
onChunkTransferred: function(reader) { },
/**
* @override
* @param {!WebInspector.ChunkedReader} reader
* @param {!Event} event
*/
onError: function(reader, event) { }
}
/**
* @constructor
* @param {!WebInspector.TracingModel.Event} event
*/
WebInspector.InvalidationTrackingEvent = function(event)
{
/** @type {string} */
this.type = event.name;
/** @type {number} */
this.startTime = event.startTime;
/** @type {!WebInspector.TracingModel.Event} */
this._tracingEvent = event;
var eventData = event.args["data"];
/** @type {number} */
this.frame = eventData["frame"];
/** @type {?number} */
this.nodeId = eventData["nodeId"];
/** @type {?string} */
this.nodeName = eventData["nodeName"];
/** @type {?number} */
this.paintId = eventData["paintId"];
/** @type {?number} */
this.invalidationSet = eventData["invalidationSet"];
/** @type {?string} */
this.invalidatedSelectorId = eventData["invalidatedSelectorId"];
/** @type {?string} */
this.changedId = eventData["changedId"];
/** @type {?string} */
this.changedClass = eventData["changedClass"];
/** @type {?string} */
this.changedAttribute = eventData["changedAttribute"];
/** @type {?string} */
this.changedPseudo = eventData["changedPseudo"];
/** @type {?string} */
this.selectorPart = eventData["selectorPart"];
/** @type {?string} */
this.extraData = eventData["extraData"];
/** @type {?Array.<!Object.<string, number>>} */
this.invalidationList = eventData["invalidationList"];
/** @type {!WebInspector.InvalidationCause} */
this.cause = {reason: eventData["reason"], stackTrace: eventData["stackTrace"]};
// FIXME: Move this to TimelineUIUtils.js.
if (!this.cause.reason && this.cause.stackTrace && this.type === WebInspector.TimelineModel.RecordType.LayoutInvalidationTracking)
this.cause.reason = "Layout forced";
}
/** @typedef {{reason: string, stackTrace: ?Array.<!ConsoleAgent.CallFrame>}} */
WebInspector.InvalidationCause;
/**
* @constructor
*/
WebInspector.InvalidationTracker = function()
{
this._initializePerFrameState();
}
WebInspector.InvalidationTracker.prototype = {
/**
* @param {!WebInspector.InvalidationTrackingEvent} invalidation
*/
addInvalidation: function(invalidation)
{
this._startNewFrameIfNeeded();
if (!invalidation.nodeId && !invalidation.paintId) {
console.error("Invalidation lacks node information.");
console.error(invalidation);
return;
}
// PaintInvalidationTracking events provide a paintId and a nodeId which
// we can use to update the paintId for all other invalidation tracking
// events.
var recordTypes = WebInspector.TimelineModel.RecordType;
if (invalidation.type === recordTypes.PaintInvalidationTracking && invalidation.nodeId) {
var invalidations = this._invalidationsByNodeId[invalidation.nodeId] || [];
for (var i = 0; i < invalidations.length; ++i)
invalidations[i].paintId = invalidation.paintId;
// PaintInvalidationTracking is only used for updating paintIds.
return;
}
// Suppress StyleInvalidator StyleRecalcInvalidationTracking invalidations because they
// will be handled by StyleInvalidatorInvalidationTracking.
// FIXME: Investigate if we can remove StyleInvalidator invalidations entirely.
if (invalidation.type === recordTypes.StyleRecalcInvalidationTracking && invalidation.cause.reason === "StyleInvalidator")
return;
// Style invalidation events can occur before and during recalc style. didRecalcStyle
// handles style invalidations that occur before the recalc style event but we need to
// handle style recalc invalidations during recalc style here.
var styleRecalcInvalidation = (invalidation.type === recordTypes.ScheduleStyleInvalidationTracking
|| invalidation.type === recordTypes.StyleInvalidatorInvalidationTracking
|| invalidation.type === recordTypes.StyleRecalcInvalidationTracking);
if (styleRecalcInvalidation) {
var duringRecalcStyle = invalidation.startTime && this._lastRecalcStyle
&& invalidation.startTime >= this._lastRecalcStyle.startTime
&& invalidation.startTime <= this._lastRecalcStyle.endTime;
if (duringRecalcStyle)
this._associateWithLastRecalcStyleEvent(invalidation);
}
// Record the invalidation so later events can look it up.
if (this._invalidations[invalidation.type])
this._invalidations[invalidation.type].push(invalidation);
else
this._invalidations[invalidation.type] = [ invalidation ];
if (invalidation.nodeId) {
if (this._invalidationsByNodeId[invalidation.nodeId])
this._invalidationsByNodeId[invalidation.nodeId].push(invalidation);
else
this._invalidationsByNodeId[invalidation.nodeId] = [ invalidation ];
}
},
/**
* @param {!WebInspector.TracingModel.Event} recalcStyleEvent
*/
didRecalcStyle: function(recalcStyleEvent)
{
this._lastRecalcStyle = recalcStyleEvent;
var types = [WebInspector.TimelineModel.RecordType.ScheduleStyleInvalidationTracking,
WebInspector.TimelineModel.RecordType.StyleInvalidatorInvalidationTracking,
WebInspector.TimelineModel.RecordType.StyleRecalcInvalidationTracking];
for (var invalidation of this._invalidationsOfTypes(types))
this._associateWithLastRecalcStyleEvent(invalidation);
},
/**
* @param {!WebInspector.InvalidationTrackingEvent} invalidation
*/
_associateWithLastRecalcStyleEvent: function(invalidation)
{
if (invalidation.linkedRecalcStyleEvent)
return;
var recordTypes = WebInspector.TimelineModel.RecordType;
var recalcStyleFrameId = this._lastRecalcStyle.args["beginData"]["frame"];
if (invalidation.type === recordTypes.StyleInvalidatorInvalidationTracking) {
// Instead of calling _addInvalidationToEvent directly, we create synthetic
// StyleRecalcInvalidationTracking events which will be added in _addInvalidationToEvent.
this._addSyntheticStyleRecalcInvalidations(this._lastRecalcStyle, recalcStyleFrameId, invalidation);
} else if (invalidation.type === recordTypes.ScheduleStyleInvalidationTracking) {
// ScheduleStyleInvalidationTracking events are only used for adding information to
// StyleInvalidatorInvalidationTracking events. See: _addSyntheticStyleRecalcInvalidations.
} else {
this._addInvalidationToEvent(this._lastRecalcStyle, recalcStyleFrameId, invalidation);
}
invalidation.linkedRecalcStyleEvent = true;
},
/**
* @param {!WebInspector.TracingModel.Event} event
* @param {number} frameId
* @param {!WebInspector.InvalidationTrackingEvent} styleInvalidatorInvalidation
*/
_addSyntheticStyleRecalcInvalidations: function(event, frameId, styleInvalidatorInvalidation)
{
if (!styleInvalidatorInvalidation.invalidationList) {
this._addSyntheticStyleRecalcInvalidation(styleInvalidatorInvalidation._tracingEvent, styleInvalidatorInvalidation);
return;
}
if (!styleInvalidatorInvalidation.nodeId) {
console.error("Invalidation lacks node information.");
console.error(invalidation);
return;
}
for (var i = 0; i < styleInvalidatorInvalidation.invalidationList.length; i++) {
var setId = styleInvalidatorInvalidation.invalidationList[i]["id"];
var lastScheduleStyleRecalculation;
var nodeInvalidations = this._invalidationsByNodeId[styleInvalidatorInvalidation.nodeId] || [];
for (var j = 0; j < nodeInvalidations.length; j++) {
var invalidation = nodeInvalidations[j];
if (invalidation.frame !== frameId || invalidation.invalidationSet !== setId || invalidation.type !== WebInspector.TimelineModel.RecordType.ScheduleStyleInvalidationTracking)
continue;
lastScheduleStyleRecalculation = invalidation;
}
if (!lastScheduleStyleRecalculation) {
console.error("Failed to lookup the event that scheduled a style invalidator invalidation.");
continue;
}
this._addSyntheticStyleRecalcInvalidation(lastScheduleStyleRecalculation._tracingEvent, styleInvalidatorInvalidation);
}
},
/**
* @param {!WebInspector.TracingModel.Event} baseEvent
* @param {!WebInspector.InvalidationTrackingEvent} styleInvalidatorInvalidation
*/
_addSyntheticStyleRecalcInvalidation: function(baseEvent, styleInvalidatorInvalidation)
{
var invalidation = new WebInspector.InvalidationTrackingEvent(baseEvent);
invalidation.type = WebInspector.TimelineModel.RecordType.StyleRecalcInvalidationTracking;
invalidation.synthetic = true;
if (styleInvalidatorInvalidation.cause.reason)
invalidation.cause.reason = styleInvalidatorInvalidation.cause.reason;
if (styleInvalidatorInvalidation.selectorPart)
invalidation.selectorPart = styleInvalidatorInvalidation.selectorPart;
this.addInvalidation(invalidation);
if (!invalidation.linkedRecalcStyleEvent)
this._associateWithLastRecalcStyleEvent(invalidation);
},
/**
* @param {!WebInspector.TracingModel.Event} layoutEvent
*/
didLayout: function(layoutEvent)
{
var layoutFrameId = layoutEvent.args["beginData"]["frame"];
for (var invalidation of this._invalidationsOfTypes([WebInspector.TimelineModel.RecordType.LayoutInvalidationTracking])) {
if (invalidation.linkedLayoutEvent)
continue;
this._addInvalidationToEvent(layoutEvent, layoutFrameId, invalidation);
invalidation.linkedLayoutEvent = true;
}
},
/**
* @param {!WebInspector.TracingModel.Event} paintEvent
*/
didPaint: function(paintEvent)
{
this._didPaint = true;
// If a paint doesn't have a corresponding graphics layer id, it paints
// into its parent so add an effectivePaintId to these events.
var layerId = paintEvent.args["data"]["layerId"];
if (layerId)
this._lastPaintWithLayer = paintEvent;
if (!this._lastPaintWithLayer) {
console.error("Failed to find a paint container for a paint event.");
return;
}
var effectivePaintId = this._lastPaintWithLayer.args["data"]["nodeId"];
var paintFrameId = paintEvent.args["data"]["frame"];
var types = [WebInspector.TimelineModel.RecordType.StyleRecalcInvalidationTracking,
WebInspector.TimelineModel.RecordType.LayoutInvalidationTracking,
WebInspector.TimelineModel.RecordType.PaintInvalidationTracking,
WebInspector.TimelineModel.RecordType.ScrollInvalidationTracking];
for (var invalidation of this._invalidationsOfTypes(types)) {
if (invalidation.paintId === effectivePaintId)
this._addInvalidationToEvent(paintEvent, paintFrameId, invalidation);
}
},
/**
* @param {!WebInspector.TracingModel.Event} event
* @param {number} eventFrameId
* @param {!WebInspector.InvalidationTrackingEvent} invalidation
*/
_addInvalidationToEvent: function(event, eventFrameId, invalidation)
{
if (eventFrameId !== invalidation.frame)
return;
if (!event.invalidationTrackingEvents)
event.invalidationTrackingEvents = [ invalidation ];
else
event.invalidationTrackingEvents.push(invalidation);
},
/**
* @param {!Array.<string>=} types
* @return {!Iterator.<!WebInspector.InvalidationTrackingEvent>}
*/
_invalidationsOfTypes: function(types)
{
var invalidations = this._invalidations;
if (!types)
types = Object.keys(invalidations);
function* generator()
{
for (var i = 0; i < types.length; ++i) {
var invalidationList = invalidations[types[i]] || [];
for (var j = 0; j < invalidationList.length; ++j)
yield invalidationList[j];
}
}
return generator();
},
_startNewFrameIfNeeded: function()
{
if (!this._didPaint)
return;
this._initializePerFrameState();
},
_initializePerFrameState: function()
{
/** @type {!Object.<string, !Array.<!WebInspector.InvalidationTrackingEvent>>} */
this._invalidations = {};
/** @type {!Object.<number, !Array.<!WebInspector.InvalidationTrackingEvent>>} */
this._invalidationsByNodeId = {};
this._lastRecalcStyle = undefined;
this._lastPaintWithLayer = undefined;
this._didPaint = false;
}
}
/**
* @constructor
*/
WebInspector.TimelineAsyncEventTracker = function()
{
WebInspector.TimelineAsyncEventTracker._initialize();
/** @type {!Map<!WebInspector.TimelineModel.RecordType, !Map<string, !WebInspector.TracingModel.Event>>} */
this._initiatorByType = new Map();
for (var initiator of WebInspector.TimelineAsyncEventTracker._asyncEvents.keys())
this._initiatorByType.set(initiator, new Map());
}
WebInspector.TimelineAsyncEventTracker._initialize = function()
{
if (WebInspector.TimelineAsyncEventTracker._asyncEvents)
return;
var events = new Map();
var type = WebInspector.TimelineModel.RecordType;
events.set(type.TimerInstall, {causes: [type.TimerFire], joinBy: "timerId"});
events.set(type.ResourceSendRequest, {causes: [type.ResourceReceiveResponse, type.ResourceReceivedData, type.ResourceFinish], joinBy: "requestId"});
events.set(type.RequestAnimationFrame, {causes: [type.FireAnimationFrame], joinBy: "id"});
events.set(type.WebSocketCreate, {causes: [type.WebSocketSendHandshakeRequest, type.WebSocketReceiveHandshakeResponse, type.WebSocketDestroy], joinBy: "identifier"});
WebInspector.TimelineAsyncEventTracker._asyncEvents = events;
/** @type {!Map<!WebInspector.TimelineModel.RecordType, !WebInspector.TimelineModel.RecordType>} */
WebInspector.TimelineAsyncEventTracker._typeToInitiator = new Map();
for (var entry of events) {
var types = entry[1].causes;
for (type of types)
WebInspector.TimelineAsyncEventTracker._typeToInitiator.set(type, entry[0]);
}
}
WebInspector.TimelineAsyncEventTracker.prototype = {
/**
* @param {!WebInspector.TracingModel.Event} event
*/
processEvent: function(event)
{
var initiatorType = WebInspector.TimelineAsyncEventTracker._typeToInitiator.get(/** @type {!WebInspector.TimelineModel.RecordType} */ (event.name));
var isInitiator = !initiatorType;
if (!initiatorType)
initiatorType = /** @type {!WebInspector.TimelineModel.RecordType} */ (event.name);
var initiatorInfo = WebInspector.TimelineAsyncEventTracker._asyncEvents.get(initiatorType);
if (!initiatorInfo)
return;
var id = event.args["data"][initiatorInfo.joinBy];
if (!id)
return;
/** @type {!Map<string, !WebInspector.TracingModel.Event>|undefined} */
var initiatorMap = this._initiatorByType.get(initiatorType);
if (isInitiator)
initiatorMap.set(id, event);
else
event.initiator = initiatorMap.get(id) || null;
}
}