blob: b6134c5bddc9a9b2d21ce65b6bc4d91dfed2ce8f [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.
*/
/**
* @unrestricted
*/
TimelineModel.TimelineModel = class {
constructor() {
this._reset();
}
/**
* @param {!Array<!SDK.TracingModel.Event>} events
* @param {function(!SDK.TracingModel.Event)} onStartEvent
* @param {function(!SDK.TracingModel.Event)} onEndEvent
* @param {function(!SDK.TracingModel.Event,?SDK.TracingModel.Event)|undefined=} onInstantEvent
* @param {number=} startTime
* @param {number=} endTime
* @param {function(!SDK.TracingModel.Event):boolean=} filter
*/
static forEachEvent(events, onStartEvent, onEndEvent, onInstantEvent, startTime, endTime, filter) {
startTime = startTime || 0;
endTime = endTime || Infinity;
var stack = [];
var startEvent = TimelineModel.TimelineModel._topLevelEventEndingAfter(events, startTime);
for (var i = startEvent; i < events.length; ++i) {
var e = events[i];
if ((e.endTime || e.startTime) < startTime)
continue;
if (e.startTime >= endTime)
break;
if (SDK.TracingModel.isAsyncPhase(e.phase) || SDK.TracingModel.isFlowPhase(e.phase))
continue;
while (stack.length && stack.peekLast().endTime <= e.startTime)
onEndEvent(stack.pop());
if (filter && !filter(e))
continue;
if (e.duration) {
onStartEvent(e);
stack.push(e);
} else {
onInstantEvent && onInstantEvent(e, stack.peekLast() || null);
}
}
while (stack.length)
onEndEvent(stack.pop());
}
/**
* @param {!Array<!SDK.TracingModel.Event>} events
* @param {number} time
*/
static _topLevelEventEndingAfter(events, time) {
var index = events.upperBound(time, (time, event) => time - event.startTime) - 1;
while (index > 0 && !SDK.TracingModel.isTopLevelEvent(events[index]))
index--;
return Math.max(index, 0);
}
/**
* @param {!Array<!TimelineModel.TimelineModelFilter>} filters
* @param {!SDK.TracingModel.Event} event
* @return {boolean}
*/
static isVisible(filters, event) {
for (var i = 0; i < filters.length; ++i) {
if (!filters[i].accept(event))
return false;
}
return true;
}
/**
* @param {!SDK.TracingModel.Event} event
* @return {boolean}
*/
static isMarkerEvent(event) {
const recordTypes = TimelineModel.TimelineModel.RecordType;
switch (event.name) {
case recordTypes.TimeStamp:
case recordTypes.MarkFirstPaint:
case recordTypes.MarkFMP:
case recordTypes.MarkFMPCandidate:
return true;
case recordTypes.MarkDOMContent:
case recordTypes.MarkLoad:
return event.args['data']['isMainFrame'];
default:
return false;
}
}
/**
* @param {!SDK.TracingModel.Event} event
* @param {string} field
* @return {string}
*/
static globalEventId(event, field) {
var data = event.args['data'] || event.args['beginData'];
var id = data && data[field];
if (!id)
return '';
return `${event.thread.process().id()}.${id}`;
}
/**
* @param {!SDK.TracingModel.Event} event
* @return {string}
*/
static eventFrameId(event) {
return TimelineModel.TimelineModel.globalEventId(event, 'frame');
}
/**
* @return {!Array<!SDK.CPUProfileDataModel>}
*/
cpuProfiles() {
return this._cpuProfiles;
}
/**
* @return {?string}
*/
sessionId() {
return this._sessionId;
}
/**
* @param {!SDK.TracingModel.Event} event
* @return {?SDK.Target}
*/
targetByEvent(event) {
// FIXME: Consider returning null for loaded traces.
var workerId = this._workerIdByThread.get(event.thread);
var mainTarget = SDK.targetManager.mainTarget();
return workerId ? SDK.targetManager.targetById(workerId) : mainTarget;
}
/**
* @param {!SDK.TracingModel} tracingModel
* @param {boolean=} produceTraceStartedInPage
*/
setEvents(tracingModel, produceTraceStartedInPage) {
this._reset();
this._resetProcessingState();
this._minimumRecordTime = tracingModel.minimumRecordTime();
this._maximumRecordTime = tracingModel.maximumRecordTime();
var metadataEvents = this._processMetadataEvents(tracingModel, !!produceTraceStartedInPage);
if (Runtime.experiments.isEnabled('timelineShowAllProcesses')) {
var lastPageMetaEvent = metadataEvents.page.peekLast();
for (var process of tracingModel.sortedProcesses()) {
for (var thread of process.sortedThreads())
this._processThreadEvents(tracingModel, 0, Infinity, thread, thread === lastPageMetaEvent.thread);
}
} else {
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() === TimelineModel.TimelineModel.WorkerThreadName) {
var workerMetaEvent = metadataEvents.workers.find(e => e.args['data']['workerThreadId'] === thread.id());
if (!workerMetaEvent)
continue;
var workerId = workerMetaEvent.args['data']['workerId'];
if (workerId)
this._workerIdByThread.set(thread, workerId);
}
this._processThreadEvents(tracingModel, startTime, endTime, thread, thread === metaEvent.thread);
}
startTime = endTime;
}
}
this._inspectedTargetEvents.sort(SDK.TracingModel.Event.compareStartTime);
this._processBrowserEvents(tracingModel);
this._buildGPUEvents(tracingModel);
this._insertFirstPaintEvent();
this._resetProcessingState();
}
/**
* @param {!SDK.TracingModel} tracingModel
* @param {boolean} produceTraceStartedInPage
* @return {!TimelineModel.TimelineModel.MetadataEvents}
*/
_processMetadataEvents(tracingModel, produceTraceStartedInPage) {
var metadataEvents = tracingModel.devToolsMetadataEvents();
var pageDevToolsMetadataEvents = [];
var workersDevToolsMetadataEvents = [];
for (var event of metadataEvents) {
if (event.name === TimelineModel.TimelineModel.DevToolsMetadataEvent.TracingStartedInPage) {
pageDevToolsMetadataEvents.push(event);
var frames = ((event.args['data'] && event.args['data']['frames']) || []);
frames.forEach(payload => this._addPageFrame(event, payload));
var rootFrame = this.rootFrames()[0];
if (rootFrame && rootFrame.url)
this._pageURL = rootFrame.url;
} else if (event.name === TimelineModel.TimelineModel.DevToolsMetadataEvent.TracingSessionIdForWorker) {
workersDevToolsMetadataEvents.push(event);
} else if (event.name === TimelineModel.TimelineModel.DevToolsMetadataEvent.TracingStartedInBrowser) {
console.assert(!this._mainFrameNodeId, 'Multiple sessions in trace');
this._mainFrameNodeId = event.args['frameTreeNodeId'];
}
}
if (!pageDevToolsMetadataEvents.length) {
// The trace is probably coming not from DevTools. Make a mock Metadata event.
var pageMetaEvent = produceTraceStartedInPage ? this._makeMockPageMetadataEvent(tracingModel) : null;
if (!pageMetaEvent) {
console.error(TimelineModel.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 {!SDK.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(SDK.TracingModel.Event.compareStartTime),
workers: workersDevToolsMetadataEvents.filter(checkSessionId).sort(SDK.TracingModel.Event.compareStartTime)
};
if (mismatchingIds.size) {
Common.console.error(
'Timeline recording was started in more than one page simultaneously. Session id mismatch: ' +
this._sessionId + ' and ' + mismatchingIds.valuesArray() + '.');
}
return result;
}
/**
* @param {!SDK.TracingModel} tracingModel
* @return {?SDK.TracingModel.Event}
*/
_makeMockPageMetadataEvent(tracingModel) {
var rendererMainThreadName = TimelineModel.TimelineModel.RendererMainThreadName;
// FIXME: pick up the first renderer process for now.
var process = tracingModel.sortedProcesses().filter(function(p) {
return p.threadByName(rendererMainThreadName);
})[0];
var thread = process && process.threadByName(rendererMainThreadName);
if (!thread)
return null;
var pageMetaEvent = new SDK.TracingModel.Event(
SDK.TracingModel.DevToolsMetadataEventCategory,
TimelineModel.TimelineModel.DevToolsMetadataEvent.TracingStartedInPage, SDK.TracingModel.Phase.Metadata,
tracingModel.minimumRecordTime(), thread);
pageMetaEvent.addArgs({'data': {'sessionId': 'mockSessionId'}});
return pageMetaEvent;
}
_insertFirstPaintEvent() {
if (!this._firstCompositeLayers)
return;
// First Paint is actually a DrawFrame that happened after first CompositeLayers following last CommitLoadEvent.
var recordTypes = TimelineModel.TimelineModel.RecordType;
var i = this._inspectedTargetEvents.lowerBound(this._firstCompositeLayers, SDK.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 SDK.TracingModel.Event(
drawFrameEvent.categoriesString, recordTypes.MarkFirstPaint, SDK.TracingModel.Phase.Instant,
drawFrameEvent.startTime, drawFrameEvent.thread);
this._mainThreadEvents.splice(
this._mainThreadEvents.lowerBound(firstPaintEvent, SDK.TracingModel.Event.compareStartTime), 0,
firstPaintEvent);
this._eventDividers.splice(
this._eventDividers.lowerBound(firstPaintEvent, SDK.TracingModel.Event.compareStartTime), 0, firstPaintEvent);
}
/**
* @param {!SDK.TracingModel} tracingModel
*/
_processBrowserEvents(tracingModel) {
var browserMain = SDK.TracingModel.browserMainThread(tracingModel);
if (!browserMain)
return;
// Disregard regular events, we don't need them yet, but still process to get proper metadata.
browserMain.events().forEach(this._processBrowserEvent, this);
/** @type {!Map<!TimelineModel.TimelineModel.AsyncEventGroup, !Array<!SDK.TracingModel.AsyncEvent>>} */
var asyncEventsByGroup = new Map();
this._processAsyncEvents(asyncEventsByGroup, browserMain.asyncEvents());
this._mergeAsyncEvents(this._mainThreadAsyncEventsByGroup, asyncEventsByGroup);
}
/**
* @param {!SDK.TracingModel} tracingModel
*/
_buildGPUEvents(tracingModel) {
var thread = tracingModel.threadByName('GPU Process', 'CrGpuMain');
if (!thread)
return;
var gpuEventName = TimelineModel.TimelineModel.RecordType.GPUTask;
this._gpuEvents = thread.events().filter(event => event.name === gpuEventName);
}
_resetProcessingState() {
this._asyncEventTracker = new TimelineModel.TimelineAsyncEventTracker();
this._invalidationTracker = new TimelineModel.InvalidationTracker();
this._layoutInvalidate = {};
this._lastScheduleStyleRecalculation = {};
this._paintImageEventByPixelRefId = {};
this._lastPaintForLayer = {};
this._lastRecalculateStylesEvent = null;
this._currentScriptEvent = null;
this._eventStack = [];
this._hadCommitLoad = false;
this._firstCompositeLayers = null;
/** @type {!Set<string>} */
this._knownInputEvents = new Set();
this._currentPage = null;
}
/**
* @param {!SDK.TracingModel} tracingModel
* @param {!SDK.TracingModel.Thread} thread
* @return {?SDK.CPUProfileDataModel}
*/
_extractCpuProfile(tracingModel, thread) {
var events = thread.events();
var cpuProfile;
// Check for legacy CpuProfile event format first.
var cpuProfileEvent = events.peekLast();
if (cpuProfileEvent && cpuProfileEvent.name === TimelineModel.TimelineModel.RecordType.CpuProfile) {
var eventData = cpuProfileEvent.args['data'];
cpuProfile = /** @type {?Protocol.Profiler.Profile} */ (eventData && eventData['cpuProfile']);
}
if (!cpuProfile) {
cpuProfileEvent = events.find(e => e.name === TimelineModel.TimelineModel.RecordType.Profile);
if (!cpuProfileEvent)
return null;
var profileGroup = tracingModel.profileGroup(cpuProfileEvent.id);
if (!profileGroup) {
Common.console.error('Invalid CPU profile format.');
return null;
}
cpuProfile = /** @type {!Protocol.Profiler.Profile} */ (
{startTime: cpuProfileEvent.args['data']['startTime'], endTime: 0, nodes: [], samples: [], timeDeltas: []});
for (var profileEvent of profileGroup.children) {
var eventData = profileEvent.args['data'];
if ('startTime' in eventData)
cpuProfile.startTime = eventData['startTime'];
if ('endTime' in eventData)
cpuProfile.endTime = eventData['endTime'];
var nodesAndSamples = eventData['cpuProfile'] || {};
cpuProfile.nodes.pushAll(nodesAndSamples['nodes'] || []);
cpuProfile.samples.pushAll(nodesAndSamples['samples'] || []);
cpuProfile.timeDeltas.pushAll(eventData['timeDeltas'] || []);
if (cpuProfile.samples.length !== cpuProfile.timeDeltas.length) {
Common.console.error('Failed to parse CPU profile.');
return null;
}
}
if (!cpuProfile.endTime)
cpuProfile.endTime = cpuProfile.timeDeltas.reduce((x, y) => x + y, cpuProfile.startTime);
}
try {
var jsProfileModel = new SDK.CPUProfileDataModel(cpuProfile);
this._cpuProfiles.push(jsProfileModel);
return jsProfileModel;
} catch (e) {
Common.console.error('Failed to parse CPU profile.');
}
return null;
}
/**
* @param {!SDK.TracingModel} tracingModel
* @param {!SDK.TracingModel.Thread} thread
* @return {!Array<!SDK.TracingModel.Event>}
*/
_injectJSFrameEvents(tracingModel, thread) {
var jsProfileModel = this._extractCpuProfile(tracingModel, thread);
var events = thread.events();
var jsSamples = jsProfileModel ?
TimelineModel.TimelineJSProfileProcessor.generateTracingEventsFromCpuProfile(jsProfileModel, thread) :
null;
if (jsSamples && jsSamples.length)
events = events.mergeOrdered(jsSamples, SDK.TracingModel.Event.orderedCompareStartTime);
if (jsSamples || events.some(e => e.name === TimelineModel.TimelineModel.RecordType.JSSample)) {
var jsFrameEvents = TimelineModel.TimelineJSProfileProcessor.generateJSFrameEvents(events);
if (jsFrameEvents && jsFrameEvents.length)
events = jsFrameEvents.mergeOrdered(events, SDK.TracingModel.Event.orderedCompareStartTime);
}
return events;
}
/**
* @param {!SDK.TracingModel} tracingModel
* @param {number} startTime
* @param {number} endTime
* @param {!SDK.TracingModel.Thread} thread
* @param {boolean} isMainThread
*/
_processThreadEvents(tracingModel, startTime, endTime, thread, isMainThread) {
var events = this._injectJSFrameEvents(tracingModel, thread);
var asyncEvents = thread.asyncEvents();
var groupByFrame = isMainThread && Runtime.experiments.isEnabled('timelinePerFrameTrack');
var threadEvents;
var threadAsyncEventsByGroup;
if (isMainThread) {
threadEvents = this._mainThreadEvents;
threadAsyncEventsByGroup = this._mainThreadAsyncEventsByGroup;
} else {
var virtualThread = new TimelineModel.TimelineModel.VirtualThread(thread.name());
this._virtualThreads.push(virtualThread);
threadEvents = virtualThread.events;
threadAsyncEventsByGroup = virtualThread.asyncEventsByGroup;
}
this._eventStack = [];
var eventStack = this._eventStack;
var i = events.lowerBound(startTime, (time, event) => time - event.startTime);
var length = events.length;
for (; i < length; i++) {
var event = events[i];
if (endTime && event.startTime >= endTime)
break;
while (eventStack.length && eventStack.peekLast().endTime <= event.startTime)
eventStack.pop();
if (!this._processEvent(event))
continue;
if (!SDK.TracingModel.isAsyncPhase(event.phase) && event.duration) {
if (eventStack.length) {
var parent = eventStack.peekLast();
parent.selfTime -= event.duration;
if (parent.selfTime < 0)
this._fixNegativeDuration(parent, event);
}
event.selfTime = event.duration;
if (isMainThread) {
if (!eventStack.length)
this._mainThreadTasks.push(event);
}
eventStack.push(event);
}
if (isMainThread && TimelineModel.TimelineModel.isMarkerEvent(event))
this._eventDividers.push(event);
if (groupByFrame) {
var frameId = TimelineModel.TimelineData.forEvent(event).frameId;
var pageFrame = frameId && this._pageFrames.get(frameId);
var isMainFrame = !frameId || !pageFrame || !pageFrame.parent;
if (isMainFrame)
frameId = TimelineModel.TimelineModel.PageFrame.mainFrameId;
var frameEvents = this._eventsByFrame.get(frameId);
if (!frameEvents) {
frameEvents = [];
this._eventsByFrame.set(frameId, frameEvents);
}
frameEvents.push(event);
}
threadEvents.push(event);
this._inspectedTargetEvents.push(event);
}
this._processAsyncEvents(threadAsyncEventsByGroup, asyncEvents, startTime, endTime);
// Pretend the compositor's async events are on the main thread.
if (thread.name() === 'Compositor') {
this._mergeAsyncEvents(this._mainThreadAsyncEventsByGroup, threadAsyncEventsByGroup);
threadAsyncEventsByGroup.clear();
}
}
/**
* @param {!SDK.TracingModel.Event} event
* @param {!SDK.TracingModel.Event} child
*/
_fixNegativeDuration(event, child) {
var epsilon = 1e-3;
if (event.selfTime < -epsilon) {
console.error(
`Children are longer than parent at ${event.startTime} ` +
`(${(child.startTime - this.minimumRecordTime()).toFixed(3)} by ${(-event.selfTime).toFixed(3)}`);
}
event.selfTime = 0;
}
/**
* @param {!Map<!TimelineModel.TimelineModel.AsyncEventGroup, !Array<!SDK.TracingModel.AsyncEvent>>} asyncEventsByGroup
* @param {!Array<!SDK.TracingModel.AsyncEvent>} asyncEvents
* @param {number=} startTime
* @param {number=} endTime
*/
_processAsyncEvents(asyncEventsByGroup, asyncEvents, startTime, endTime) {
var i = startTime ? asyncEvents.lowerBound(startTime, function(time, asyncEvent) {
return time - asyncEvent.startTime;
}) : 0;
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 = asyncEventsByGroup.get(asyncGroup);
if (!groupAsyncEvents) {
groupAsyncEvents = [];
asyncEventsByGroup.set(asyncGroup, groupAsyncEvents);
}
groupAsyncEvents.push(asyncEvent);
}
}
/**
* @param {!SDK.TracingModel.Event} event
* @return {boolean}
*/
_processEvent(event) {
var recordTypes = TimelineModel.TimelineModel.RecordType;
var eventStack = this._eventStack;
if (!eventStack.length) {
if (this._currentTaskLayoutAndRecalcEvents && this._currentTaskLayoutAndRecalcEvents.length) {
var totalTime = this._currentTaskLayoutAndRecalcEvents.reduce((time, event) => time + event.duration, 0);
if (totalTime > TimelineModel.TimelineModel.Thresholds.ForcedLayout) {
for (var e of this._currentTaskLayoutAndRecalcEvents) {
let timelineData = TimelineModel.TimelineData.forEvent(e);
timelineData.warning = e.name === recordTypes.Layout ?
TimelineModel.TimelineModel.WarningType.ForcedLayout :
TimelineModel.TimelineModel.WarningType.ForcedStyle;
}
}
}
this._currentTaskLayoutAndRecalcEvents = [];
}
if (this._currentScriptEvent && event.startTime > this._currentScriptEvent.endTime)
this._currentScriptEvent = null;
var eventData = event.args['data'] || event.args['beginData'] || {};
var timelineData = TimelineModel.TimelineData.forEvent(event);
if (eventData['stackTrace'])
timelineData.stackTrace = eventData['stackTrace'];
if (timelineData.stackTrace && event.name !== recordTypes.JSSample) {
// TraceEvents come with 1-based line & column numbers. The frontend code
// requires 0-based ones. Adjust the values.
for (var i = 0; i < timelineData.stackTrace.length; ++i) {
--timelineData.stackTrace[i].lineNumber;
--timelineData.stackTrace[i].columnNumber;
}
}
var pageFrameId = TimelineModel.TimelineModel.eventFrameId(event);
if (!pageFrameId && eventStack.length)
pageFrameId = TimelineModel.TimelineData.forEvent(eventStack.peekLast()).frameId;
timelineData.frameId = pageFrameId || TimelineModel.TimelineModel.PageFrame.mainFrameId;
this._asyncEventTracker.processEvent(event);
switch (event.name) {
case recordTypes.ResourceSendRequest:
case recordTypes.WebSocketCreate:
timelineData.setInitiator(eventStack.peekLast() || null);
timelineData.url = eventData['url'];
break;
case recordTypes.ScheduleStyleRecalculation:
this._lastScheduleStyleRecalculation[eventData['frame']] = event;
break;
case recordTypes.UpdateLayoutTree:
case recordTypes.RecalculateStyles:
this._invalidationTracker.didRecalcStyle(event);
if (event.args['beginData'])
timelineData.setInitiator(this._lastScheduleStyleRecalculation[event.args['beginData']['frame']]);
this._lastRecalculateStylesEvent = event;
if (this._currentScriptEvent)
this._currentTaskLayoutAndRecalcEvents.push(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 TimelineModel.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 = eventData['frame'];
if (!this._layoutInvalidate[frameId] && this._lastRecalculateStylesEvent &&
this._lastRecalculateStylesEvent.endTime > event.startTime)
layoutInitator = TimelineModel.TimelineData.forEvent(this._lastRecalculateStylesEvent).initiator();
this._layoutInvalidate[frameId] = layoutInitator;
break;
case recordTypes.Layout:
this._invalidationTracker.didLayout(event);
var frameId = event.args['beginData']['frame'];
timelineData.setInitiator(this._layoutInvalidate[frameId]);
// In case we have no closing Layout event, endData is not available.
if (event.args['endData'])
timelineData.backendNodeId = event.args['endData']['rootNode'];
this._layoutInvalidate[frameId] = null;
if (this._currentScriptEvent)
this._currentTaskLayoutAndRecalcEvents.push(event);
break;
case recordTypes.EventDispatch:
if (event.duration > TimelineModel.TimelineModel.Thresholds.RecurringHandler)
timelineData.warning = TimelineModel.TimelineModel.WarningType.LongHandler;
break;
case recordTypes.TimerFire:
case recordTypes.FireAnimationFrame:
if (event.duration > TimelineModel.TimelineModel.Thresholds.RecurringHandler)
timelineData.warning = TimelineModel.TimelineModel.WarningType.LongRecurringHandler;
break;
case recordTypes.FunctionCall:
// Compatibility with old format.
if (typeof eventData['scriptName'] === 'string')
eventData['url'] = eventData['scriptName'];
if (typeof eventData['scriptLine'] === 'number')
eventData['lineNumber'] = eventData['scriptLine'];
// Fallthrough.
case recordTypes.EvaluateScript:
case recordTypes.CompileScript:
if (typeof eventData['lineNumber'] === 'number')
--eventData['lineNumber'];
if (typeof eventData['columnNumber'] === 'number')
--eventData['columnNumber'];
// Fallthrough intended.
case recordTypes.RunMicrotasks:
// Microtasks technically are not necessarily scripts, but for purpose of
// forced sync style recalc or layout detection they are.
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);
timelineData.backendNodeId = eventData['nodeId'];
// Only keep layer paint events, skip paints for subframes that get painted to the same layer as parent.
if (!eventData['layerId'])
break;
var layerId = eventData['layerId'];
this._lastPaintForLayer[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) {
TimelineModel.TimelineData.forEvent(paintEvent).picture =
/** @type {!SDK.TracingModel.ObjectSnapshot} */ (event);
}
break;
case recordTypes.ScrollLayer:
timelineData.backendNodeId = eventData['nodeId'];
break;
case recordTypes.PaintImage:
timelineData.backendNodeId = eventData['nodeId'];
timelineData.url = eventData['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;
var paintImageData = TimelineModel.TimelineData.forEvent(paintImageEvent);
timelineData.backendNodeId = paintImageData.backendNodeId;
timelineData.url = paintImageData.url;
break;
case recordTypes.DrawLazyPixelRef:
var paintImageEvent = this._findAncestorEvent(recordTypes.PaintImage);
if (!paintImageEvent)
break;
this._paintImageEventByPixelRefId[event.args['LazyPixelRef']] = paintImageEvent;
var paintImageData = TimelineModel.TimelineData.forEvent(paintImageEvent);
timelineData.backendNodeId = paintImageData.backendNodeId;
timelineData.url = paintImageData.url;
break;
case recordTypes.MarkDOMContent:
case recordTypes.MarkLoad:
var page = eventData['page'];
if (page && page !== this._currentPage)
return false;
break;
case recordTypes.CommitLoad:
var frameId = TimelineModel.TimelineModel.eventFrameId(event);
var pageFrame = this._pageFrames.get(frameId);
if (pageFrame)
pageFrame.update(eventData.name || '', eventData.url || '');
else
this._addPageFrame(event, eventData);
var page = eventData['page'];
if (page && page !== this._currentPage)
return false;
if (!eventData['isMainFrame'])
break;
if (eventData.url)
this._pageURL = eventData.url;
this._hadCommitLoad = true;
this._firstCompositeLayers = null;
break;
case recordTypes.CompositeLayers:
if (!this._firstCompositeLayers && this._hadCommitLoad)
this._firstCompositeLayers = event;
break;
case recordTypes.FireIdleCallback:
if (event.duration >
eventData['allottedMilliseconds'] + TimelineModel.TimelineModel.Thresholds.IdleCallbackAddon)
timelineData.warning = TimelineModel.TimelineModel.WarningType.IdleDeadlineExceeded;
break;
}
return true;
}
/**
* @param {!SDK.TracingModel.Event} event
*/
_processBrowserEvent(event) {
if (event.name !== TimelineModel.TimelineModel.RecordType.LatencyInfoFlow)
return;
var frameId = event.args['frameTreeNodeId'];
if (typeof frameId === 'number' && frameId === this._mainFrameNodeId)
this._knownInputEvents.add(event.bind_id);
}
/**
* @param {!SDK.TracingModel.AsyncEvent} asyncEvent
* @return {?TimelineModel.TimelineModel.AsyncEventGroup}
*/
_processAsyncEvent(asyncEvent) {
var groups = TimelineModel.TimelineModel.AsyncEventGroup;
if (asyncEvent.hasCategory(TimelineModel.TimelineModel.Category.Console))
return groups.console;
if (asyncEvent.hasCategory(TimelineModel.TimelineModel.Category.UserTiming))
return groups.userTiming;
if (asyncEvent.name === TimelineModel.TimelineModel.RecordType.Animation)
return groups.animation;
if (asyncEvent.hasCategory(TimelineModel.TimelineModel.Category.LatencyInfo) ||
asyncEvent.name === TimelineModel.TimelineModel.RecordType.ImplSideFling) {
var lastStep = asyncEvent.steps.peekLast();
// FIXME: fix event termination on the back-end instead.
if (lastStep.phase !== SDK.TracingModel.Phase.AsyncEnd)
return null;
var data = lastStep.args['data'];
asyncEvent.causedFrame = !!(data && data['INPUT_EVENT_LATENCY_RENDERER_SWAP_COMPONENT']);
if (asyncEvent.hasCategory(TimelineModel.TimelineModel.Category.LatencyInfo)) {
if (!this._knownInputEvents.has(lastStep.id))
return null;
if (asyncEvent.name === TimelineModel.TimelineModel.RecordType.InputLatencyMouseMove && !asyncEvent.causedFrame)
return null;
var rendererMain = data['INPUT_EVENT_LATENCY_RENDERER_MAIN_COMPONENT'];
if (rendererMain) {
var time = rendererMain['time'] / 1000;
TimelineModel.TimelineData.forEvent(asyncEvent.steps[0]).timeWaitingForMainThread =
time - asyncEvent.steps[0].startTime;
}
}
return groups.input;
}
return null;
}
/**
* @param {string} name
* @return {?SDK.TracingModel.Event}
*/
_findAncestorEvent(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 {!Map<!TimelineModel.TimelineModel.AsyncEventGroup, !Array<!SDK.TracingModel.AsyncEvent>>} target
* @param {!Map<!TimelineModel.TimelineModel.AsyncEventGroup, !Array<!SDK.TracingModel.AsyncEvent>>} source
*/
_mergeAsyncEvents(target, source) {
for (var group of source.keys()) {
var events = target.get(group) || [];
events = events.mergeOrdered(source.get(group) || [], SDK.TracingModel.Event.compareStartAndEndTime);
target.set(group, events);
}
}
/**
* @param {!SDK.TracingModel.Event} event
* @param {!Object} payload
*/
_addPageFrame(event, payload) {
var processId = event.thread.process().id();
var pageFrame = new TimelineModel.TimelineModel.PageFrame(this.targetByEvent(event), processId, payload);
this._pageFrames.set(pageFrame.id, pageFrame);
var parent = payload['parent'] && this._pageFrames.get(`${processId}.${payload['parent']}`);
if (parent)
parent.addChild(pageFrame);
}
_reset() {
this._virtualThreads = [];
/** @type {!Array<!SDK.TracingModel.Event>} */
this._mainThreadEvents = [];
/** @type {!Map<!TimelineModel.TimelineModel.AsyncEventGroup, !Array<!SDK.TracingModel.AsyncEvent>>} */
this._mainThreadAsyncEventsByGroup = new Map();
/** @type {!Array<!SDK.TracingModel.Event>} */
this._inspectedTargetEvents = [];
/** @type {!Array<!SDK.TracingModel.Event>} */
this._mainThreadTasks = [];
/** @type {!Array<!SDK.TracingModel.Event>} */
this._gpuEvents = [];
/** @type {!Array<!SDK.TracingModel.Event>} */
this._eventDividers = [];
/** @type {?string} */
this._sessionId = null;
/** @type {?number} */
this._mainFrameNodeId = null;
/** @type {!Array<!SDK.CPUProfileDataModel>} */
this._cpuProfiles = [];
/** @type {!WeakMap<!SDK.TracingModel.Thread, string>} */
this._workerIdByThread = new WeakMap();
/** @type {!Map<string, !TimelineModel.TimelineModel.PageFrame>} */
this._pageFrames = new Map();
/** @type {!Map<string, !Array<!SDK.TracingModel.Event>>} */
this._eventsByFrame = new Map();
this._pageURL = '';
this._minimumRecordTime = 0;
this._maximumRecordTime = 0;
}
/**
* @return {number}
*/
minimumRecordTime() {
return this._minimumRecordTime;
}
/**
* @return {number}
*/
maximumRecordTime() {
return this._maximumRecordTime;
}
/**
* @return {!Array<!SDK.TracingModel.Event>}
*/
inspectedTargetEvents() {
return this._inspectedTargetEvents;
}
/**
* @return {!Array<!SDK.TracingModel.Event>}
*/
mainThreadEvents() {
return this._mainThreadEvents;
}
/**
* @return {!Map<!TimelineModel.TimelineModel.AsyncEventGroup, !Array.<!SDK.TracingModel.AsyncEvent>>}
*/
mainThreadAsyncEvents() {
return this._mainThreadAsyncEventsByGroup;
}
/**
* @return {!Array<!TimelineModel.TimelineModel.VirtualThread>}
*/
virtualThreads() {
return this._virtualThreads;
}
/**
* @return {boolean}
*/
isEmpty() {
return this.minimumRecordTime() === 0 && this.maximumRecordTime() === 0;
}
/**
* @return {!Array<!SDK.TracingModel.Event>}
*/
mainThreadTasks() {
return this._mainThreadTasks;
}
/**
* @return {!Array<!SDK.TracingModel.Event>}
*/
gpuEvents() {
return this._gpuEvents;
}
/**
* @return {!Array<!SDK.TracingModel.Event>}
*/
eventDividers() {
return this._eventDividers;
}
/**
* @return {!Array<!TimelineModel.TimelineModel.PageFrame>}
*/
rootFrames() {
return Array.from(this._pageFrames.values()).filter(frame => !frame.parent);
}
/**
* @return {string}
*/
pageURL() {
return this._pageURL;
}
/**
* @param {string} frameId
* @return {?TimelineModel.TimelineModel.PageFrame}
*/
pageFrameById(frameId) {
return frameId ? this._pageFrames.get(frameId) || null : null;
}
/**
* @param {string} frameId
* @return {!Array<!SDK.TracingModel.Event>}
*/
eventsForFrame(frameId) {
return this._eventsByFrame.get(frameId) || [];
}
/**
* @return {!Array<!TimelineModel.TimelineModel.NetworkRequest>}
*/
networkRequests() {
/** @type {!Map<string,!TimelineModel.TimelineModel.NetworkRequest>} */
var requests = new Map();
/** @type {!Array<!TimelineModel.TimelineModel.NetworkRequest>} */
var requestsList = [];
/** @type {!Array<!TimelineModel.TimelineModel.NetworkRequest>} */
var zeroStartRequestsList = [];
var types = TimelineModel.TimelineModel.RecordType;
var resourceTypes = new Set(
[types.ResourceSendRequest, types.ResourceReceiveResponse, types.ResourceReceivedData, types.ResourceFinish]);
var events = this.inspectedTargetEvents();
for (var i = 0; i < events.length; ++i) {
var e = events[i];
if (!resourceTypes.has(e.name))
continue;
var id = TimelineModel.TimelineModel.globalEventId(e, 'requestId');
var request = requests.get(id);
if (request) {
request.addEvent(e);
} else {
request = new TimelineModel.TimelineModel.NetworkRequest(e);
requests.set(id, request);
if (request.startTime)
requestsList.push(request);
else
zeroStartRequestsList.push(request);
}
}
return zeroStartRequestsList.concat(requestsList);
}
};
/**
* @enum {string}
*/
TimelineModel.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',
CompileScript: 'v8.compile',
EvaluateScript: 'EvaluateScript',
CommitLoad: 'CommitLoad',
MarkLoad: 'MarkLoad',
MarkDOMContent: 'MarkDOMContent',
MarkFirstPaint: 'MarkFirstPaint',
MarkFMP: 'firstMeaningfulPaint',
MarkFMPCandidate: 'firstMeaningfulPaintCandidate',
TimeStamp: 'TimeStamp',
ConsoleTime: 'ConsoleTime',
UserTiming: 'UserTiming',
ResourceSendRequest: 'ResourceSendRequest',
ResourceReceiveResponse: 'ResourceReceiveResponse',
ResourceReceivedData: 'ResourceReceivedData',
ResourceFinish: 'ResourceFinish',
RunMicrotasks: 'RunMicrotasks',
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',
ParseScriptOnBackground: 'v8.parseOnBackground',
UpdateCounters: 'UpdateCounters',
RequestAnimationFrame: 'RequestAnimationFrame',
CancelAnimationFrame: 'CancelAnimationFrame',
FireAnimationFrame: 'FireAnimationFrame',
RequestIdleCallback: 'RequestIdleCallback',
CancelIdleCallback: 'CancelIdleCallback',
FireIdleCallback: 'FireIdleCallback',
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',
LatencyInfo: 'LatencyInfo',
LatencyInfoFlow: 'LatencyInfo.Flow',
InputLatencyMouseMove: 'InputLatency::MouseMove',
InputLatencyMouseWheel: 'InputLatency::MouseWheel',
ImplSideFling: 'InputHandlerProxy::HandleGestureFling::started',
GCIdleLazySweep: 'ThreadState::performIdleLazySweep',
GCCompleteSweep: 'ThreadState::completeSweep',
GCCollectGarbage: 'BlinkGCMarking',
// CpuProfile is a virtual event created on frontend to support
// serialization of CPU Profiles within tracing timeline data.
CpuProfile: 'CpuProfile',
Profile: 'Profile',
AsyncTask: 'AsyncTask',
};
TimelineModel.TimelineModel.Category = {
Console: 'blink.console',
UserTiming: 'blink.user_timing',
LatencyInfo: 'latencyInfo'
};
/**
* @enum {string}
*/
TimelineModel.TimelineModel.WarningType = {
ForcedStyle: 'ForcedStyle',
ForcedLayout: 'ForcedLayout',
IdleDeadlineExceeded: 'IdleDeadlineExceeded',
LongHandler: 'LongHandler',
LongRecurringHandler: 'LongRecurringHandler',
V8Deopt: 'V8Deopt'
};
TimelineModel.TimelineModel.MainThreadName = 'main';
TimelineModel.TimelineModel.WorkerThreadName = 'DedicatedWorker Thread';
TimelineModel.TimelineModel.RendererMainThreadName = 'CrRendererMain';
/**
* @enum {symbol}
*/
TimelineModel.TimelineModel.AsyncEventGroup = {
animation: Symbol('animation'),
console: Symbol('console'),
userTiming: Symbol('userTiming'),
input: Symbol('input')
};
TimelineModel.TimelineModel.DevToolsMetadataEvent = {
TracingStartedInBrowser: 'TracingStartedInBrowser',
TracingStartedInPage: 'TracingStartedInPage',
TracingSessionIdForWorker: 'TracingSessionIdForWorker',
};
TimelineModel.TimelineModel.Thresholds = {
Handler: 150,
RecurringHandler: 50,
ForcedLayout: 30,
IdleCallbackAddon: 5
};
/**
* @unrestricted
*/
TimelineModel.TimelineModel.VirtualThread = class {
/**
* @param {string} name
*/
constructor(name) {
this.name = name;
/** @type {!Array<!SDK.TracingModel.Event>} */
this.events = [];
/** @type {!Map<!TimelineModel.TimelineModel.AsyncEventGroup, !Array<!SDK.TracingModel.AsyncEvent>>} */
this.asyncEventsByGroup = new Map();
}
/**
* @return {boolean}
*/
isWorker() {
return this.name === TimelineModel.TimelineModel.WorkerThreadName;
}
};
/** @typedef {!{page: !Array<!SDK.TracingModel.Event>, workers: !Array<!SDK.TracingModel.Event>}} */
TimelineModel.TimelineModel.MetadataEvents;
TimelineModel.TimelineModel.PageFrame = class {
/**
* @param {?SDK.Target} target
* @param {number} pid
* @param {!Object} payload
*/
constructor(target, pid, payload) {
this.frameId = payload['frame'];
this.url = payload['url'] || '';
this.name = payload['name'];
this.processId = pid;
this.children = [];
/** @type {?TimelineModel.TimelineModel.PageFrame} */
this.parent = null;
this.id = `${this.processId}.${this.frameId}`;
this.ownerNode = target && payload['nodeId'] ? new SDK.DeferredDOMNode(target, payload['nodeId']) : null;
}
/**
* @param {string} name
* @param {string} url
*/
update(name, url) {
this.name = name;
this.url = url;
}
/**
* @param {!TimelineModel.TimelineModel.PageFrame} child
*/
addChild(child) {
this.children.push(child);
child.parent = this;
}
};
TimelineModel.TimelineModel.PageFrame.mainFrameId = '';
/**
* @unrestricted
*/
TimelineModel.TimelineModel.NetworkRequest = class {
/**
* @param {!SDK.TracingModel.Event} event
*/
constructor(event) {
this.startTime = event.name === TimelineModel.TimelineModel.RecordType.ResourceSendRequest ? event.startTime : 0;
this.endTime = Infinity;
this.encodedDataLength = 0;
this.decodedBodyLength = 0;
/** @type {!Array<!SDK.TracingModel.Event>} */
this.children = [];
/** @type {?Object} */
this.timing;
/** @type {string} */
this.mimeType;
/** @type {string} */
this.url;
/** @type {string} */
this.requestMethod;
this.addEvent(event);
}
/**
* @param {!SDK.TracingModel.Event} event
*/
addEvent(event) {
this.children.push(event);
var recordType = TimelineModel.TimelineModel.RecordType;
this.startTime = Math.min(this.startTime, event.startTime);
var eventData = event.args['data'];
if (eventData['mimeType'])
this.mimeType = eventData['mimeType'];
if ('priority' in eventData)
this.priority = eventData['priority'];
if (event.name === recordType.ResourceFinish)
this.endTime = event.startTime;
if (eventData['finishTime'])
this.finishTime = eventData['finishTime'] * 1000;
if (!this.responseTime &&
(event.name === recordType.ResourceReceiveResponse || event.name === recordType.ResourceReceivedData))
this.responseTime = event.startTime;
var encodedDataLength = eventData['encodedDataLength'] || 0;
if (event.name === recordType.ResourceReceiveResponse) {
if (eventData['fromCache'])
this.fromCache = true;
if (eventData['fromServiceWorker'])
this.fromServiceWorker = true;
this.encodedDataLength = encodedDataLength;
}
if (event.name === recordType.ResourceReceivedData)
this.encodedDataLength += encodedDataLength;
if (event.name === recordType.ResourceFinish && encodedDataLength)
this.encodedDataLength = encodedDataLength;
var decodedBodyLength = eventData['decodedBodyLength'];
if (event.name === recordType.ResourceFinish && decodedBodyLength)
this.decodedBodyLength = decodedBodyLength;
if (!this.url)
this.url = eventData['url'];
if (!this.requestMethod)
this.requestMethod = eventData['requestMethod'];
if (!this.timing)
this.timing = eventData['timing'];
if (eventData['fromServiceWorker'])
this.fromServiceWorker = true;
}
};
/**
* @unrestricted
*/
TimelineModel.InvalidationTrackingEvent = class {
/**
* @param {!SDK.TracingModel.Event} event
*/
constructor(event) {
/** @type {string} */
this.type = event.name;
/** @type {number} */
this.startTime = event.startTime;
/** @type {!SDK.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 {!TimelineModel.InvalidationCause} */
this.cause = {reason: eventData['reason'], stackTrace: eventData['stackTrace']};
// FIXME: Move this to TimelineUIUtils.js.
if (!this.cause.reason && this.cause.stackTrace &&
this.type === TimelineModel.TimelineModel.RecordType.LayoutInvalidationTracking)
this.cause.reason = 'Layout forced';
}
};
/** @typedef {{reason: string, stackTrace: ?Array<!Protocol.Runtime.CallFrame>}} */
TimelineModel.InvalidationCause;
TimelineModel.InvalidationTracker = class {
constructor() {
/** @type {?SDK.TracingModel.Event} */
this._lastRecalcStyle = null;
/** @type {?SDK.TracingModel.Event} */
this._lastPaintWithLayer = null;
this._didPaint = false;
this._initializePerFrameState();
}
/**
* @param {!SDK.TracingModel.Event} event
* @return {?Array<!TimelineModel.InvalidationTrackingEvent>}
*/
static invalidationEventsFor(event) {
return event[TimelineModel.InvalidationTracker._invalidationTrackingEventsSymbol] || null;
}
/**
* @param {!TimelineModel.InvalidationTrackingEvent} invalidation
*/
addInvalidation(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 = TimelineModel.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 {!SDK.TracingModel.Event} recalcStyleEvent
*/
didRecalcStyle(recalcStyleEvent) {
this._lastRecalcStyle = recalcStyleEvent;
var types = [
TimelineModel.TimelineModel.RecordType.ScheduleStyleInvalidationTracking,
TimelineModel.TimelineModel.RecordType.StyleInvalidatorInvalidationTracking,
TimelineModel.TimelineModel.RecordType.StyleRecalcInvalidationTracking
];
for (var invalidation of this._invalidationsOfTypes(types))
this._associateWithLastRecalcStyleEvent(invalidation);
}
/**
* @param {!TimelineModel.InvalidationTrackingEvent} invalidation
*/
_associateWithLastRecalcStyleEvent(invalidation) {
if (invalidation.linkedRecalcStyleEvent)
return;
var recordTypes = TimelineModel.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 {!SDK.TracingModel.Event} event
* @param {number} frameId
* @param {!TimelineModel.InvalidationTrackingEvent} styleInvalidatorInvalidation
*/
_addSyntheticStyleRecalcInvalidations(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 !== TimelineModel.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 {!SDK.TracingModel.Event} baseEvent
* @param {!TimelineModel.InvalidationTrackingEvent} styleInvalidatorInvalidation
*/
_addSyntheticStyleRecalcInvalidation(baseEvent, styleInvalidatorInvalidation) {
var invalidation = new TimelineModel.InvalidationTrackingEvent(baseEvent);
invalidation.type = TimelineModel.TimelineModel.RecordType.StyleRecalcInvalidationTracking;
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 {!SDK.TracingModel.Event} layoutEvent
*/
didLayout(layoutEvent) {
var layoutFrameId = layoutEvent.args['beginData']['frame'];
for (var invalidation of this._invalidationsOfTypes(
[TimelineModel.TimelineModel.RecordType.LayoutInvalidationTracking])) {
if (invalidation.linkedLayoutEvent)
continue;
this._addInvalidationToEvent(layoutEvent, layoutFrameId, invalidation);
invalidation.linkedLayoutEvent = true;
}
}
/**
* @param {!SDK.TracingModel.Event} paintEvent
*/
didPaint(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;
// Quietly discard top-level paints without layerId, as these are likely
// to come from overlay.
if (!this._lastPaintWithLayer)
return;
var effectivePaintId = this._lastPaintWithLayer.args['data']['nodeId'];
var paintFrameId = paintEvent.args['data']['frame'];
var types = [
TimelineModel.TimelineModel.RecordType.StyleRecalcInvalidationTracking,
TimelineModel.TimelineModel.RecordType.LayoutInvalidationTracking,
TimelineModel.TimelineModel.RecordType.PaintInvalidationTracking,
TimelineModel.TimelineModel.RecordType.ScrollInvalidationTracking
];
for (var invalidation of this._invalidationsOfTypes(types)) {
if (invalidation.paintId === effectivePaintId)
this._addInvalidationToEvent(paintEvent, paintFrameId, invalidation);
}
}
/**
* @param {!SDK.TracingModel.Event} event
* @param {number} eventFrameId
* @param {!TimelineModel.InvalidationTrackingEvent} invalidation
*/
_addInvalidationToEvent(event, eventFrameId, invalidation) {
if (eventFrameId !== invalidation.frame)
return;
if (!event[TimelineModel.InvalidationTracker._invalidationTrackingEventsSymbol])
event[TimelineModel.InvalidationTracker._invalidationTrackingEventsSymbol] = [invalidation];
else
event[TimelineModel.InvalidationTracker._invalidationTrackingEventsSymbol].push(invalidation);
}
/**
* @param {!Array.<string>=} types
* @return {!Iterator.<!TimelineModel.InvalidationTrackingEvent>}
*/
_invalidationsOfTypes(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() {
if (!this._didPaint)
return;
this._initializePerFrameState();
}
_initializePerFrameState() {
/** @type {!Object.<string, !Array.<!TimelineModel.InvalidationTrackingEvent>>} */
this._invalidations = {};
/** @type {!Object.<number, !Array.<!TimelineModel.InvalidationTrackingEvent>>} */
this._invalidationsByNodeId = {};
this._lastRecalcStyle = null;
this._lastPaintWithLayer = null;
this._didPaint = false;
}
};
TimelineModel.InvalidationTracker._invalidationTrackingEventsSymbol = Symbol('invalidationTrackingEvents');
/**
* @unrestricted
*/
TimelineModel.TimelineAsyncEventTracker = class {
constructor() {
TimelineModel.TimelineAsyncEventTracker._initialize();
/** @type {!Map<!TimelineModel.TimelineModel.RecordType, !Map<string, !SDK.TracingModel.Event>>} */
this._initiatorByType = new Map();
for (var initiator of TimelineModel.TimelineAsyncEventTracker._asyncEvents.keys())
this._initiatorByType.set(initiator, new Map());
}
static _initialize() {
if (TimelineModel.TimelineAsyncEventTracker._asyncEvents)
return;
var events = new Map();
var type = TimelineModel.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.RequestIdleCallback, {causes: [type.FireIdleCallback], joinBy: 'id'});
events.set(type.WebSocketCreate, {
causes: [type.WebSocketSendHandshakeRequest, type.WebSocketReceiveHandshakeResponse, type.WebSocketDestroy],
joinBy: 'identifier'
});
TimelineModel.TimelineAsyncEventTracker._asyncEvents = events;
/** @type {!Map<!TimelineModel.TimelineModel.RecordType, !TimelineModel.TimelineModel.RecordType>} */
TimelineModel.TimelineAsyncEventTracker._typeToInitiator = new Map();
for (var entry of events) {
var types = entry[1].causes;
for (type of types)
TimelineModel.TimelineAsyncEventTracker._typeToInitiator.set(type, entry[0]);
}
}
/**
* @param {!SDK.TracingModel.Event} event
*/
processEvent(event) {
var initiatorType = TimelineModel.TimelineAsyncEventTracker._typeToInitiator.get(
/** @type {!TimelineModel.TimelineModel.RecordType} */ (event.name));
var isInitiator = !initiatorType;
if (!initiatorType)
initiatorType = /** @type {!TimelineModel.TimelineModel.RecordType} */ (event.name);
var initiatorInfo = TimelineModel.TimelineAsyncEventTracker._asyncEvents.get(initiatorType);
if (!initiatorInfo)
return;
var id = TimelineModel.TimelineModel.globalEventId(event, initiatorInfo.joinBy);
if (!id)
return;
/** @type {!Map<string, !SDK.TracingModel.Event>|undefined} */
var initiatorMap = this._initiatorByType.get(initiatorType);
if (isInitiator) {
initiatorMap.set(id, event);
return;
}
var initiator = initiatorMap.get(id) || null;
var timelineData = TimelineModel.TimelineData.forEvent(event);
timelineData.setInitiator(initiator);
if (!timelineData.frameId && initiator)
timelineData.frameId = TimelineModel.TimelineModel.eventFrameId(initiator);
}
};
TimelineModel.TimelineData = class {
constructor() {
/** @type {?string} */
this.warning = null;
/** @type {?Element} */
this.previewElement = null;
/** @type {?string} */
this.url = null;
/** @type {number} */
this.backendNodeId = 0;
/** @type {?Array<!Protocol.Runtime.CallFrame>} */
this.stackTrace = null;
/** @type {?SDK.TracingModel.ObjectSnapshot} */
this.picture = null;
/** @type {?SDK.TracingModel.Event} */
this._initiator = null;
this.frameId = '';
/** @type {number|undefined} */
this.timeWaitingForMainThread;
}
/**
* @param {!SDK.TracingModel.Event} initiator
*/
setInitiator(initiator) {
this._initiator = initiator;
if (!initiator || this.url)
return;
var initiatorURL = TimelineModel.TimelineData.forEvent(initiator).url;
if (initiatorURL)
this.url = initiatorURL;
}
/**
* @return {?SDK.TracingModel.Event}
*/
initiator() {
return this._initiator;
}
/**
* @return {?Protocol.Runtime.CallFrame}
*/
topFrame() {
var stackTrace = this.stackTraceForSelfOrInitiator();
return stackTrace && stackTrace[0] || null;
}
/**
* @return {?Array<!Protocol.Runtime.CallFrame>}
*/
stackTraceForSelfOrInitiator() {
return this.stackTrace || (this._initiator && TimelineModel.TimelineData.forEvent(this._initiator).stackTrace);
}
/**
* @param {!SDK.TracingModel.Event} event
* @return {!TimelineModel.TimelineData}
*/
static forEvent(event) {
var data = event[TimelineModel.TimelineData._symbol];
if (!data) {
data = new TimelineModel.TimelineData();
event[TimelineModel.TimelineData._symbol] = data;
}
return data;
}
};
TimelineModel.TimelineData._symbol = Symbol('timelineData');