| /* |
| * 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 |
| */ |
| export class TimelineModelImpl { |
| 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; |
| const stack = []; |
| const startEvent = TimelineModelImpl._topLevelEventEndingAfter(events, startTime); |
| for (let i = startEvent; i < events.length; ++i) { |
| const 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) { |
| let 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 {!SDK.TracingModel.Event} event |
| * @return {boolean} |
| */ |
| isMarkerEvent(event) { |
| const recordTypes = RecordType; |
| switch (event.name) { |
| case recordTypes.TimeStamp: |
| return true; |
| case recordTypes.MarkFirstPaint: |
| case recordTypes.MarkFCP: |
| case recordTypes.MarkFMP: |
| // TODO(alph): There are duplicate FMP events coming from the backend. Keep the one having 'data' property. |
| return this._mainFrame && event.args.frame === this._mainFrame.frameId && !!event.args.data; |
| case recordTypes.MarkDOMContent: |
| case recordTypes.MarkLoad: |
| case recordTypes.MarkLCPCandidate: |
| case recordTypes.MarkLCPInvalidate: |
| return !!event.args['data']['isMainFrame']; |
| default: |
| return false; |
| } |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| * @return {boolean} |
| */ |
| isLCPCandidateEvent(event) { |
| return event.name === RecordType.MarkLCPCandidate && !!event.args['data']['isMainFrame']; |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| * @return {boolean} |
| */ |
| isLCPInvalidateEvent(event) { |
| return event.name === RecordType.MarkLCPInvalidate && !!event.args['data']['isMainFrame']; |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| * @param {string} field |
| * @return {string} |
| */ |
| static globalEventId(event, field) { |
| const data = event.args['data'] || event.args['beginData']; |
| const id = data && data[field]; |
| if (!id) { |
| return ''; |
| } |
| return `${event.thread.process().id()}.${id}`; |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| * @return {string} |
| */ |
| static eventFrameId(event) { |
| const data = event.args['data'] || event.args['beginData']; |
| return data && data['frame'] || ''; |
| } |
| |
| /** |
| * @return {!Array<!SDK.CPUProfileDataModel>} |
| */ |
| cpuProfiles() { |
| return this._cpuProfiles; |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| * @return {?SDK.Target} |
| */ |
| targetByEvent(event) { |
| // FIXME: Consider returning null for loaded traces. |
| const workerId = this._workerIdByThread.get(event.thread); |
| const mainTarget = SDK.targetManager.mainTarget(); |
| return workerId ? SDK.targetManager.targetById(workerId) : mainTarget; |
| } |
| |
| /** |
| * @param {!SDK.TracingModel} tracingModel |
| */ |
| setEvents(tracingModel) { |
| this._reset(); |
| this._resetProcessingState(); |
| this._tracingModel = tracingModel; |
| |
| this._minimumRecordTime = tracingModel.minimumRecordTime(); |
| this._maximumRecordTime = tracingModel.maximumRecordTime(); |
| |
| this._processSyncBrowserEvents(tracingModel); |
| if (this._browserFrameTracking) { |
| this._processThreadsForBrowserFrames(tracingModel); |
| } else { |
| // The next line is for loading legacy traces recorded before M67. |
| // TODO(alph): Drop the support at some point. |
| const metadataEvents = this._processMetadataEvents(tracingModel); |
| this._isGenericTrace = !metadataEvents; |
| if (metadataEvents) { |
| this._processMetadataAndThreads(tracingModel, metadataEvents); |
| } else { |
| this._processGenericTrace(tracingModel); |
| } |
| } |
| this._inspectedTargetEvents.sort(SDK.TracingModel.Event.compareStartTime); |
| this._processAsyncBrowserEvents(tracingModel); |
| this._buildGPUEvents(tracingModel); |
| this._resetProcessingState(); |
| } |
| |
| /** |
| * @param {!SDK.TracingModel} tracingModel |
| */ |
| _processGenericTrace(tracingModel) { |
| let browserMainThread = SDK.TracingModel.browserMainThread(tracingModel); |
| if (!browserMainThread && tracingModel.sortedProcesses().length) { |
| browserMainThread = tracingModel.sortedProcesses()[0].sortedThreads()[0]; |
| } |
| for (const process of tracingModel.sortedProcesses()) { |
| for (const thread of process.sortedThreads()) { |
| this._processThreadEvents( |
| tracingModel, [{from: 0, to: Infinity}], thread, thread === browserMainThread, false, true, null); |
| } |
| } |
| } |
| |
| /** |
| * @param {!SDK.TracingModel} tracingModel |
| * @param {!TimelineModel.TimelineModel.MetadataEvents} metadataEvents |
| */ |
| _processMetadataAndThreads(tracingModel, metadataEvents) { |
| let startTime = 0; |
| for (let i = 0, length = metadataEvents.page.length; i < length; i++) { |
| const metaEvent = metadataEvents.page[i]; |
| const process = metaEvent.thread.process(); |
| const endTime = i + 1 < length ? metadataEvents.page[i + 1].startTime : Infinity; |
| if (startTime === endTime) { |
| continue; |
| } |
| this._legacyCurrentPage = metaEvent.args['data'] && metaEvent.args['data']['page']; |
| for (const thread of process.sortedThreads()) { |
| let workerUrl = null; |
| if (thread.name() === TimelineModelImpl.WorkerThreadName || |
| thread.name() === TimelineModelImpl.WorkerThreadNameLegacy) { |
| const workerMetaEvent = metadataEvents.workers.find(e => { |
| if (e.args['data']['workerThreadId'] !== thread.id()) { |
| return false; |
| } |
| // This is to support old traces. |
| if (e.args['data']['sessionId'] === this._sessionId) { |
| return true; |
| } |
| return !!this._pageFrames.get(TimelineModelImpl.eventFrameId(e)); |
| }); |
| if (!workerMetaEvent) { |
| continue; |
| } |
| const workerId = workerMetaEvent.args['data']['workerId']; |
| if (workerId) { |
| this._workerIdByThread.set(thread, workerId); |
| } |
| workerUrl = workerMetaEvent.args['data']['url'] || ''; |
| } |
| this._processThreadEvents( |
| tracingModel, [{from: startTime, to: endTime}], thread, thread === metaEvent.thread, !!workerUrl, true, |
| workerUrl); |
| } |
| startTime = endTime; |
| } |
| } |
| |
| /** |
| * @param {!SDK.TracingModel} tracingModel |
| */ |
| _processThreadsForBrowserFrames(tracingModel) { |
| const processData = new Map(); |
| for (const frame of this._pageFrames.values()) { |
| for (let i = 0; i < frame.processes.length; i++) { |
| const pid = frame.processes[i].processId; |
| let data = processData.get(pid); |
| if (!data) { |
| data = []; |
| processData.set(pid, data); |
| } |
| const to = i === frame.processes.length - 1 ? (frame.deletedTime || Infinity) : frame.processes[i + 1].time; |
| data.push({from: frame.processes[i].time, to: to, main: !frame.parent, url: frame.processes[i].url}); |
| } |
| } |
| const allMetadataEvents = tracingModel.devToolsMetadataEvents(); |
| for (const process of tracingModel.sortedProcesses()) { |
| const data = processData.get(process.id()); |
| if (!data) { |
| continue; |
| } |
| data.sort((a, b) => a.from - b.from || a.to - b.to); |
| const ranges = []; |
| let lastUrl = null; |
| let lastMainUrl = null; |
| let hasMain = false; |
| for (const item of data) { |
| if (!ranges.length || item.from > ranges.peekLast().to) { |
| ranges.push({from: item.from, to: item.to}); |
| } else { |
| ranges.peekLast().to = item.to; |
| } |
| if (item.main) { |
| hasMain = true; |
| } |
| if (item.url) { |
| if (item.main) { |
| lastMainUrl = item.url; |
| } |
| lastUrl = item.url; |
| } |
| } |
| |
| for (const thread of process.sortedThreads()) { |
| if (thread.name() === TimelineModelImpl.RendererMainThreadName) { |
| this._processThreadEvents( |
| tracingModel, ranges, thread, true /* isMainThread */, false /* isWorker */, hasMain, |
| hasMain ? lastMainUrl : lastUrl); |
| } else if ( |
| thread.name() === TimelineModelImpl.WorkerThreadName || |
| thread.name() === TimelineModelImpl.WorkerThreadNameLegacy) { |
| const workerMetaEvent = allMetadataEvents.find(e => { |
| if (e.name !== TimelineModelImpl.DevToolsMetadataEvent.TracingSessionIdForWorker) { |
| return false; |
| } |
| if (e.thread.process() !== process) { |
| return false; |
| } |
| if (e.args['data']['workerThreadId'] !== thread.id()) { |
| return false; |
| } |
| return !!this._pageFrames.get(TimelineModelImpl.eventFrameId(e)); |
| }); |
| if (!workerMetaEvent) { |
| continue; |
| } |
| this._workerIdByThread.set(thread, workerMetaEvent.args['data']['workerId'] || ''); |
| this._processThreadEvents( |
| tracingModel, ranges, thread, false /* isMainThread */, true /* isWorker */, false /* forMainFrame */, |
| workerMetaEvent.args['data']['url'] || ''); |
| } else { |
| this._processThreadEvents( |
| tracingModel, ranges, thread, false /* isMainThread */, false /* isWorker */, false /* forMainFrame */, |
| null); |
| } |
| } |
| } |
| } |
| |
| /** |
| * @param {!SDK.TracingModel} tracingModel |
| * @return {?TimelineModel.TimelineModel.MetadataEvents} |
| */ |
| _processMetadataEvents(tracingModel) { |
| const metadataEvents = tracingModel.devToolsMetadataEvents(); |
| |
| const pageDevToolsMetadataEvents = []; |
| const workersDevToolsMetadataEvents = []; |
| for (const event of metadataEvents) { |
| if (event.name === TimelineModelImpl.DevToolsMetadataEvent.TracingStartedInPage) { |
| pageDevToolsMetadataEvents.push(event); |
| if (event.args['data'] && event.args['data']['persistentIds']) { |
| this._persistentIds = true; |
| } |
| const frames = ((event.args['data'] && event.args['data']['frames']) || []); |
| frames.forEach(payload => this._addPageFrame(event, payload)); |
| this._mainFrame = this.rootFrames()[0]; |
| } else if (event.name === TimelineModelImpl.DevToolsMetadataEvent.TracingSessionIdForWorker) { |
| workersDevToolsMetadataEvents.push(event); |
| } else if (event.name === TimelineModelImpl.DevToolsMetadataEvent.TracingStartedInBrowser) { |
| console.assert(!this._mainFrameNodeId, 'Multiple sessions in trace'); |
| this._mainFrameNodeId = event.args['frameTreeNodeId']; |
| } |
| } |
| if (!pageDevToolsMetadataEvents.length) { |
| return null; |
| } |
| |
| const sessionId = |
| pageDevToolsMetadataEvents[0].args['sessionId'] || pageDevToolsMetadataEvents[0].args['data']['sessionId']; |
| this._sessionId = sessionId; |
| |
| const mismatchingIds = new Set(); |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| * @return {boolean} |
| */ |
| function checkSessionId(event) { |
| let args = event.args; |
| // FIXME: put sessionId into args["data"] for TracingStartedInPage event. |
| if (args['data']) { |
| args = args['data']; |
| } |
| const id = args['sessionId']; |
| if (id === sessionId) { |
| return true; |
| } |
| mismatchingIds.add(id); |
| return false; |
| } |
| const result = { |
| page: pageDevToolsMetadataEvents.filter(checkSessionId).sort(SDK.TracingModel.Event.compareStartTime), |
| workers: workersDevToolsMetadataEvents.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 |
| */ |
| _processSyncBrowserEvents(tracingModel) { |
| const browserMain = SDK.TracingModel.browserMainThread(tracingModel); |
| if (browserMain) { |
| browserMain.events().forEach(this._processBrowserEvent, this); |
| } |
| } |
| |
| /** |
| * @param {!SDK.TracingModel} tracingModel |
| */ |
| _processAsyncBrowserEvents(tracingModel) { |
| const browserMain = SDK.TracingModel.browserMainThread(tracingModel); |
| if (browserMain) { |
| this._processAsyncEvents(browserMain, [{from: 0, to: Infinity}]); |
| } |
| } |
| |
| /** |
| * @param {!SDK.TracingModel} tracingModel |
| */ |
| _buildGPUEvents(tracingModel) { |
| const thread = tracingModel.threadByName('GPU Process', 'CrGpuMain'); |
| if (!thread) { |
| return; |
| } |
| const gpuEventName = RecordType.GPUTask; |
| const track = this._ensureNamedTrack(TrackType.GPU); |
| track.thread = thread; |
| track.events = thread.events().filter(event => event.name === gpuEventName); |
| } |
| |
| _resetProcessingState() { |
| this._asyncEventTracker = new TimelineAsyncEventTracker(); |
| this._invalidationTracker = new InvalidationTracker(); |
| this._layoutInvalidate = {}; |
| this._lastScheduleStyleRecalculation = {}; |
| this._paintImageEventByPixelRefId = {}; |
| this._lastPaintForLayer = {}; |
| this._lastRecalculateStylesEvent = null; |
| this._currentScriptEvent = null; |
| this._eventStack = []; |
| /** @type {!Set<string>} */ |
| this._knownInputEvents = new Set(); |
| this._browserFrameTracking = false; |
| this._persistentIds = false; |
| this._legacyCurrentPage = null; |
| } |
| |
| /** |
| * @param {!SDK.TracingModel} tracingModel |
| * @param {!SDK.TracingModel.Thread} thread |
| * @return {?SDK.CPUProfileDataModel} |
| */ |
| _extractCpuProfile(tracingModel, thread) { |
| const events = thread.events(); |
| let cpuProfile; |
| let target = null; |
| |
| // Check for legacy CpuProfile event format first. |
| let cpuProfileEvent = events.peekLast(); |
| if (cpuProfileEvent && cpuProfileEvent.name === RecordType.CpuProfile) { |
| const eventData = cpuProfileEvent.args['data']; |
| cpuProfile = /** @type {?Protocol.Profiler.Profile} */ (eventData && eventData['cpuProfile']); |
| target = this.targetByEvent(cpuProfileEvent); |
| } |
| |
| if (!cpuProfile) { |
| cpuProfileEvent = events.find(e => e.name === RecordType.Profile); |
| if (!cpuProfileEvent) { |
| return null; |
| } |
| target = this.targetByEvent(cpuProfileEvent); |
| const profileGroup = tracingModel.profileGroup(cpuProfileEvent); |
| 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: [], |
| lines: [] |
| }); |
| for (const profileEvent of profileGroup.children) { |
| const eventData = profileEvent.args['data']; |
| if ('startTime' in eventData) { |
| cpuProfile.startTime = eventData['startTime']; |
| } |
| if ('endTime' in eventData) { |
| cpuProfile.endTime = eventData['endTime']; |
| } |
| const nodesAndSamples = eventData['cpuProfile'] || {}; |
| const samples = nodesAndSamples['samples'] || []; |
| const lines = eventData['lines'] || Array(samples.length).fill(0); |
| cpuProfile.nodes.pushAll(nodesAndSamples['nodes'] || []); |
| cpuProfile.lines.pushAll(lines); |
| cpuProfile.samples.pushAll(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 { |
| const jsProfileModel = new SDK.CPUProfileDataModel(cpuProfile, target); |
| 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) { |
| const jsProfileModel = this._extractCpuProfile(tracingModel, thread); |
| let events = thread.events(); |
| const 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 === RecordType.JSSample)) { |
| const jsFrameEvents = TimelineModel.TimelineJSProfileProcessor.generateJSFrameEvents(events); |
| if (jsFrameEvents && jsFrameEvents.length) { |
| events = jsFrameEvents.mergeOrdered(events, SDK.TracingModel.Event.orderedCompareStartTime); |
| } |
| } |
| return events; |
| } |
| |
| /** |
| * @param {!SDK.TracingModel} tracingModel |
| * @param {!Array<!{from: number, to: number}>} ranges |
| * @param {!SDK.TracingModel.Thread} thread |
| * @param {boolean} isMainThread |
| * @param {boolean} isWorker |
| * @param {boolean} forMainFrame |
| * @param {?string} url |
| */ |
| _processThreadEvents(tracingModel, ranges, thread, isMainThread, isWorker, forMainFrame, url) { |
| const track = new Track(); |
| track.name = thread.name() || ls`Thread ${thread.id()}`; |
| track.type = TrackType.Other; |
| track.thread = thread; |
| if (isMainThread) { |
| track.type = TrackType.MainThread; |
| track.url = url || null; |
| track.forMainFrame = forMainFrame; |
| } else if (isWorker) { |
| track.type = TrackType.Worker; |
| track.url = url; |
| } else if (thread.name().startsWith('CompositorTileWorker')) { |
| track.type = TrackType.Raster; |
| } |
| this._tracks.push(track); |
| |
| const events = this._injectJSFrameEvents(tracingModel, thread); |
| this._eventStack = []; |
| const eventStack = this._eventStack; |
| |
| for (const range of ranges) { |
| let i = events.lowerBound(range.from, (time, event) => time - event.startTime); |
| for (; i < events.length; i++) { |
| const event = events[i]; |
| if (event.startTime >= range.to) { |
| 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) { |
| const parent = eventStack.peekLast(); |
| parent.selfTime -= event.duration; |
| if (parent.selfTime < 0) { |
| this._fixNegativeDuration(parent, event); |
| } |
| } |
| event.selfTime = event.duration; |
| if (!eventStack.length) { |
| track.tasks.push(event); |
| } |
| eventStack.push(event); |
| } |
| if (this.isMarkerEvent(event)) { |
| this._timeMarkerEvents.push(event); |
| } |
| |
| track.events.push(event); |
| this._inspectedTargetEvents.push(event); |
| } |
| } |
| this._processAsyncEvents(thread, ranges); |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| * @param {!SDK.TracingModel.Event} child |
| */ |
| _fixNegativeDuration(event, child) { |
| const 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 {!SDK.TracingModel.Thread} thread |
| * @param {!Array<!{from: number, to: number}>} ranges |
| */ |
| _processAsyncEvents(thread, ranges) { |
| const asyncEvents = thread.asyncEvents(); |
| const groups = new Map(); |
| |
| /** |
| * @param {!TrackType} type |
| * @return {!Array<!SDK.TracingModel.AsyncEvent>} |
| */ |
| function group(type) { |
| if (!groups.has(type)) { |
| groups.set(type, []); |
| } |
| return groups.get(type); |
| } |
| |
| for (const range of ranges) { |
| let i = asyncEvents.lowerBound(range.from, function(time, asyncEvent) { |
| return time - asyncEvent.startTime; |
| }); |
| |
| for (; i < asyncEvents.length; ++i) { |
| const asyncEvent = asyncEvents[i]; |
| if (asyncEvent.startTime >= range.to) { |
| break; |
| } |
| |
| if (asyncEvent.hasCategory(TimelineModelImpl.Category.Console)) { |
| group(TrackType.Console).push(asyncEvent); |
| continue; |
| } |
| |
| if (asyncEvent.hasCategory(TimelineModelImpl.Category.UserTiming)) { |
| group(TrackType.Timings).push(asyncEvent); |
| continue; |
| } |
| |
| if (asyncEvent.name === RecordType.Animation) { |
| group(TrackType.Animation).push(asyncEvent); |
| continue; |
| } |
| |
| if (asyncEvent.hasCategory(TimelineModelImpl.Category.LatencyInfo) || |
| asyncEvent.name === RecordType.ImplSideFling) { |
| const lastStep = asyncEvent.steps.peekLast(); |
| // FIXME: fix event termination on the back-end instead. |
| if (lastStep.phase !== SDK.TracingModel.Phase.AsyncEnd) { |
| continue; |
| } |
| const data = lastStep.args['data']; |
| asyncEvent.causedFrame = !!(data && data['INPUT_EVENT_LATENCY_RENDERER_SWAP_COMPONENT']); |
| if (asyncEvent.hasCategory(TimelineModelImpl.Category.LatencyInfo)) { |
| if (!this._knownInputEvents.has(lastStep.id)) { |
| continue; |
| } |
| if (asyncEvent.name === RecordType.InputLatencyMouseMove && !asyncEvent.causedFrame) { |
| continue; |
| } |
| // Coalesced events are not really been processed, no need to track them. |
| if (data['is_coalesced']) { |
| continue; |
| } |
| const rendererMain = data['INPUT_EVENT_LATENCY_RENDERER_MAIN_COMPONENT']; |
| if (rendererMain) { |
| const time = rendererMain['time'] / 1000; |
| TimelineData.forEvent(asyncEvent.steps[0]).timeWaitingForMainThread = |
| time - asyncEvent.steps[0].startTime; |
| } |
| } |
| group(TrackType.Input).push(asyncEvent); |
| continue; |
| } |
| } |
| } |
| |
| for (const [type, events] of groups) { |
| const track = this._ensureNamedTrack(type); |
| track.thread = thread; |
| track.asyncEvents = track.asyncEvents.mergeOrdered(events, SDK.TracingModel.Event.compareStartTime); |
| } |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| * @return {boolean} |
| */ |
| _processEvent(event) { |
| const recordTypes = RecordType; |
| const eventStack = this._eventStack; |
| |
| if (!eventStack.length) { |
| if (this._currentTaskLayoutAndRecalcEvents && this._currentTaskLayoutAndRecalcEvents.length) { |
| const totalTime = this._currentTaskLayoutAndRecalcEvents.reduce((time, event) => time + event.duration, 0); |
| if (totalTime > TimelineModelImpl.Thresholds.ForcedLayout) { |
| for (const e of this._currentTaskLayoutAndRecalcEvents) { |
| const timelineData = TimelineData.forEvent(e); |
| timelineData.warning = e.name === recordTypes.Layout ? TimelineModelImpl.WarningType.ForcedLayout : |
| TimelineModelImpl.WarningType.ForcedStyle; |
| } |
| } |
| } |
| this._currentTaskLayoutAndRecalcEvents = []; |
| } |
| |
| if (this._currentScriptEvent && event.startTime > this._currentScriptEvent.endTime) { |
| this._currentScriptEvent = null; |
| } |
| |
| const eventData = event.args['data'] || event.args['beginData'] || {}; |
| const timelineData = 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 (let i = 0; i < timelineData.stackTrace.length; ++i) { |
| --timelineData.stackTrace[i].lineNumber; |
| --timelineData.stackTrace[i].columnNumber; |
| } |
| } |
| let pageFrameId = TimelineModelImpl.eventFrameId(event); |
| if (!pageFrameId && eventStack.length) { |
| pageFrameId = TimelineData.forEvent(eventStack.peekLast()).frameId; |
| } |
| timelineData.frameId = pageFrameId || (this._mainFrame && this._mainFrame.frameId) || ''; |
| this._asyncEventTracker.processEvent(event); |
| |
| if (this.isMarkerEvent(event)) { |
| this._ensureNamedTrack(TrackType.Timings); |
| } |
| |
| 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: |
| this._invalidationTracker.addInvalidation(new 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. |
| let layoutInitator = event; |
| const frameId = eventData['frame']; |
| if (!this._layoutInvalidate[frameId] && this._lastRecalculateStylesEvent && |
| this._lastRecalculateStylesEvent.endTime > event.startTime) { |
| layoutInitator = TimelineData.forEvent(this._lastRecalculateStylesEvent).initiator(); |
| } |
| this._layoutInvalidate[frameId] = layoutInitator; |
| break; |
| } |
| |
| case recordTypes.Layout: { |
| this._invalidationTracker.didLayout(event); |
| const 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.Task: |
| if (event.duration > TimelineModelImpl.Thresholds.LongTask) { |
| timelineData.warning = TimelineModelImpl.WarningType.LongTask; |
| } |
| break; |
| |
| case recordTypes.EventDispatch: |
| if (event.duration > TimelineModelImpl.Thresholds.RecurringHandler) { |
| timelineData.warning = TimelineModelImpl.WarningType.LongHandler; |
| } |
| break; |
| |
| case recordTypes.TimerFire: |
| case recordTypes.FireAnimationFrame: |
| if (event.duration > TimelineModelImpl.Thresholds.RecurringHandler) { |
| timelineData.warning = TimelineModelImpl.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 is to support old traces. |
| if (this._sessionId && eventData['sessionId'] && this._sessionId === eventData['sessionId']) { |
| this._mainFrameLayerTreeId = eventData['layerTreeId']; |
| break; |
| } |
| |
| // We currently only show layer tree for the main frame. |
| const frameId = TimelineModelImpl.eventFrameId(event); |
| const pageFrame = this._pageFrames.get(frameId); |
| if (!pageFrame || pageFrame.parent) { |
| return false; |
| } |
| this._mainFrameLayerTreeId = eventData['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; |
| } |
| const layerId = eventData['layerId']; |
| this._lastPaintForLayer[layerId] = event; |
| break; |
| } |
| |
| case recordTypes.DisplayItemListSnapshot: |
| case recordTypes.PictureSnapshot: { |
| const layerUpdateEvent = this._findAncestorEvent(recordTypes.UpdateLayer); |
| if (!layerUpdateEvent || layerUpdateEvent.args['layerTreeId'] !== this._mainFrameLayerTreeId) { |
| break; |
| } |
| const paintEvent = this._lastPaintForLayer[layerUpdateEvent.args['layerId']]; |
| if (paintEvent) { |
| 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: { |
| let paintImageEvent = this._findAncestorEvent(recordTypes.PaintImage); |
| if (!paintImageEvent) { |
| const decodeLazyPixelRefEvent = this._findAncestorEvent(recordTypes.DecodeLazyPixelRef); |
| paintImageEvent = decodeLazyPixelRefEvent && |
| this._paintImageEventByPixelRefId[decodeLazyPixelRefEvent.args['LazyPixelRef']]; |
| } |
| if (!paintImageEvent) { |
| break; |
| } |
| const paintImageData = TimelineData.forEvent(paintImageEvent); |
| timelineData.backendNodeId = paintImageData.backendNodeId; |
| timelineData.url = paintImageData.url; |
| break; |
| } |
| |
| case recordTypes.DrawLazyPixelRef: { |
| const paintImageEvent = this._findAncestorEvent(recordTypes.PaintImage); |
| if (!paintImageEvent) { |
| break; |
| } |
| this._paintImageEventByPixelRefId[event.args['LazyPixelRef']] = paintImageEvent; |
| const paintImageData = TimelineData.forEvent(paintImageEvent); |
| timelineData.backendNodeId = paintImageData.backendNodeId; |
| timelineData.url = paintImageData.url; |
| break; |
| } |
| |
| case recordTypes.FrameStartedLoading: |
| if (timelineData.frameId !== event.args['frame']) { |
| return false; |
| } |
| break; |
| |
| case recordTypes.MarkLCPCandidate: |
| timelineData.backendNodeId = eventData['nodeId']; |
| break; |
| |
| case recordTypes.MarkDOMContent: |
| case recordTypes.MarkLoad: { |
| const frameId = TimelineModelImpl.eventFrameId(event); |
| if (!this._pageFrames.has(frameId)) { |
| return false; |
| } |
| break; |
| } |
| |
| case recordTypes.CommitLoad: { |
| if (this._browserFrameTracking) { |
| break; |
| } |
| const frameId = TimelineModelImpl.eventFrameId(event); |
| const isMainFrame = !!eventData['isMainFrame']; |
| const pageFrame = this._pageFrames.get(frameId); |
| if (pageFrame) { |
| pageFrame.update(event.startTime, eventData); |
| } else { |
| // We should only have one main frame which has persistent id, |
| // unless it's an old trace without 'persistentIds' flag. |
| if (!this._persistentIds) { |
| if (eventData['page'] && eventData['page'] !== this._legacyCurrentPage) { |
| return false; |
| } |
| } else if (isMainFrame) { |
| return false; |
| } else if (!this._addPageFrame(event, eventData)) { |
| return false; |
| } |
| } |
| if (isMainFrame) { |
| this._mainFrame = this._pageFrames.get(frameId); |
| } |
| break; |
| } |
| |
| case recordTypes.FireIdleCallback: |
| if (event.duration > eventData['allottedMilliseconds'] + TimelineModelImpl.Thresholds.IdleCallbackAddon) { |
| timelineData.warning = TimelineModelImpl.WarningType.IdleDeadlineExceeded; |
| } |
| break; |
| } |
| return true; |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| */ |
| _processBrowserEvent(event) { |
| if (event.name === RecordType.LatencyInfoFlow) { |
| const frameId = event.args['frameTreeNodeId']; |
| if (typeof frameId === 'number' && frameId === this._mainFrameNodeId) { |
| this._knownInputEvents.add(event.bind_id); |
| } |
| return; |
| } |
| |
| if (event.name === RecordType.ResourceWillSendRequest) { |
| const requestId = event.args['data']['requestId']; |
| if (typeof requestId === 'string') { |
| this._requestsFromBrowser.set(requestId, event); |
| } |
| return; |
| } |
| |
| if (event.hasCategory(SDK.TracingModel.DevToolsMetadataEventCategory) && event.args['data']) { |
| const data = event.args['data']; |
| if (event.name === TimelineModelImpl.DevToolsMetadataEvent.TracingStartedInBrowser) { |
| if (!data['persistentIds']) { |
| return; |
| } |
| this._browserFrameTracking = true; |
| this._mainFrameNodeId = data['frameTreeNodeId']; |
| const frames = data['frames'] || []; |
| frames.forEach(payload => { |
| const parent = payload['parent'] && this._pageFrames.get(payload['parent']); |
| if (payload['parent'] && !parent) { |
| return; |
| } |
| let frame = this._pageFrames.get(payload['frame']); |
| if (!frame) { |
| frame = new PageFrame(payload); |
| this._pageFrames.set(frame.frameId, frame); |
| if (parent) { |
| parent.addChild(frame); |
| } else { |
| this._mainFrame = frame; |
| } |
| } |
| // TODO(dgozman): this should use event.startTime, but due to races between tracing start |
| // in different processes we cannot do this yet. |
| frame.update(this._minimumRecordTime, payload); |
| }); |
| return; |
| } |
| if (event.name === TimelineModelImpl.DevToolsMetadataEvent.FrameCommittedInBrowser && |
| this._browserFrameTracking) { |
| let frame = this._pageFrames.get(data['frame']); |
| if (!frame) { |
| const parent = data['parent'] && this._pageFrames.get(data['parent']); |
| if (!parent) { |
| return; |
| } |
| frame = new PageFrame(data); |
| this._pageFrames.set(frame.frameId, frame); |
| parent.addChild(frame); |
| } |
| frame.update(event.startTime, data); |
| return; |
| } |
| if (event.name === TimelineModelImpl.DevToolsMetadataEvent.ProcessReadyInBrowser && this._browserFrameTracking) { |
| const frame = this._pageFrames.get(data['frame']); |
| if (frame) { |
| frame.processReady(data['processPseudoId'], data['processId']); |
| } |
| return; |
| } |
| if (event.name === TimelineModelImpl.DevToolsMetadataEvent.FrameDeletedInBrowser && this._browserFrameTracking) { |
| const frame = this._pageFrames.get(data['frame']); |
| if (frame) { |
| frame.deletedTime = event.startTime; |
| } |
| return; |
| } |
| } |
| } |
| |
| /** |
| * @param {!TrackType} type |
| * @return {!Track} |
| */ |
| _ensureNamedTrack(type) { |
| if (!this._namedTracks.has(type)) { |
| const track = new Track(); |
| track.type = type; |
| this._tracks.push(track); |
| this._namedTracks.set(type, track); |
| } |
| return this._namedTracks.get(type); |
| } |
| |
| /** |
| * @param {string} name |
| * @return {?SDK.TracingModel.Event} |
| */ |
| _findAncestorEvent(name) { |
| for (let i = this._eventStack.length - 1; i >= 0; --i) { |
| const event = this._eventStack[i]; |
| if (event.name === name) { |
| return event; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| * @param {!Object} payload |
| * @return {boolean} |
| */ |
| _addPageFrame(event, payload) { |
| const parent = payload['parent'] && this._pageFrames.get(payload['parent']); |
| if (payload['parent'] && !parent) { |
| return false; |
| } |
| const pageFrame = new PageFrame(payload); |
| this._pageFrames.set(pageFrame.frameId, pageFrame); |
| pageFrame.update(event.startTime, payload); |
| if (parent) { |
| parent.addChild(pageFrame); |
| } |
| return true; |
| } |
| |
| _reset() { |
| this._isGenericTrace = false; |
| /** @type {!Array<!Track>} */ |
| this._tracks = []; |
| /** @type {!Map<!TrackType, !Track>} */ |
| this._namedTracks = new Map(); |
| /** @type {!Array<!SDK.TracingModel.Event>} */ |
| this._inspectedTargetEvents = []; |
| /** @type {!Array<!SDK.TracingModel.Event>} */ |
| this._timeMarkerEvents = []; |
| /** @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, !PageFrame>} */ |
| this._pageFrames = new Map(); |
| this._mainFrame = null; |
| /** @type {!Map<string, !SDK.TracingModel.Event>} */ |
| this._requestsFromBrowser = new Map(); |
| |
| this._minimumRecordTime = 0; |
| this._maximumRecordTime = 0; |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| isGenericTrace() { |
| return this._isGenericTrace; |
| } |
| |
| /** |
| * @return {!SDK.TracingModel} |
| */ |
| tracingModel() { |
| return this._tracingModel; |
| } |
| |
| /** |
| * @return {number} |
| */ |
| minimumRecordTime() { |
| return this._minimumRecordTime; |
| } |
| |
| /** |
| * @return {number} |
| */ |
| maximumRecordTime() { |
| return this._maximumRecordTime; |
| } |
| |
| /** |
| * @return {!Array<!SDK.TracingModel.Event>} |
| */ |
| inspectedTargetEvents() { |
| return this._inspectedTargetEvents; |
| } |
| |
| /** |
| * @return {!Array<!Track>} |
| */ |
| tracks() { |
| return this._tracks; |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| isEmpty() { |
| return this.minimumRecordTime() === 0 && this.maximumRecordTime() === 0; |
| } |
| |
| /** |
| * @return {!Array<!SDK.TracingModel.Event>} |
| */ |
| timeMarkerEvents() { |
| return this._timeMarkerEvents; |
| } |
| |
| /** |
| * @return {!Array<!PageFrame>} |
| */ |
| rootFrames() { |
| return Array.from(this._pageFrames.values()).filter(frame => !frame.parent); |
| } |
| |
| /** |
| * @return {string} |
| */ |
| pageURL() { |
| return this._mainFrame && this._mainFrame.url || ''; |
| } |
| |
| /** |
| * @param {string} frameId |
| * @return {?PageFrame} |
| */ |
| pageFrameById(frameId) { |
| return frameId ? this._pageFrames.get(frameId) || null : null; |
| } |
| |
| /** |
| * @return {!Array<!NetworkRequest>} |
| */ |
| networkRequests() { |
| if (this.isGenericTrace()) { |
| return []; |
| } |
| /** @type {!Map<string,!NetworkRequest>} */ |
| const requests = new Map(); |
| /** @type {!Array<!NetworkRequest>} */ |
| const requestsList = []; |
| /** @type {!Array<!NetworkRequest>} */ |
| const zeroStartRequestsList = []; |
| const types = RecordType; |
| const resourceTypes = new Set([ |
| types.ResourceWillSendRequest, types.ResourceSendRequest, types.ResourceReceiveResponse, |
| types.ResourceReceivedData, types.ResourceFinish, types.ResourceMarkAsCached |
| ]); |
| const events = this.inspectedTargetEvents(); |
| for (let i = 0; i < events.length; ++i) { |
| const e = events[i]; |
| if (!resourceTypes.has(e.name)) { |
| continue; |
| } |
| const id = TimelineModelImpl.globalEventId(e, 'requestId'); |
| if (e.name === types.ResourceSendRequest && this._requestsFromBrowser.has(e.args.data.requestId)) { |
| addRequest(this._requestsFromBrowser.get(e.args.data.requestId), id); |
| } |
| addRequest(e, id); |
| } |
| function addRequest(e, id) { |
| let request = requests.get(id); |
| if (request) { |
| request.addEvent(e); |
| } else { |
| request = new NetworkRequest(e); |
| requests.set(id, request); |
| if (request.startTime) { |
| requestsList.push(request); |
| } else { |
| zeroStartRequestsList.push(request); |
| } |
| } |
| } |
| return zeroStartRequestsList.concat(requestsList); |
| } |
| } |
| |
| /** |
| * @enum {string} |
| */ |
| export const RecordType = { |
| Task: 'RunTask', |
| 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', |
| |
| ParseHTML: 'ParseHTML', |
| ParseAuthorStyleSheet: 'ParseAuthorStyleSheet', |
| |
| TimerInstall: 'TimerInstall', |
| TimerRemove: 'TimerRemove', |
| TimerFire: 'TimerFire', |
| |
| XHRReadyStateChange: 'XHRReadyStateChange', |
| XHRLoad: 'XHRLoad', |
| CompileScript: 'v8.compile', |
| EvaluateScript: 'EvaluateScript', |
| CompileModule: 'v8.compileModule', |
| EvaluateModule: 'v8.evaluateModule', |
| WasmStreamFromResponseCallback: 'v8.wasm.streamFromResponseCallback', |
| WasmCompiledModule: 'v8.wasm.compiledModule', |
| WasmCachedModule: 'v8.wasm.cachedModule', |
| WasmModuleCacheHit: 'v8.wasm.moduleCacheHit', |
| WasmModuleCacheInvalid: 'v8.wasm.moduleCacheInvalid', |
| |
| FrameStartedLoading: 'FrameStartedLoading', |
| CommitLoad: 'CommitLoad', |
| MarkLoad: 'MarkLoad', |
| MarkDOMContent: 'MarkDOMContent', |
| MarkFirstPaint: 'firstPaint', |
| MarkFCP: 'firstContentfulPaint', |
| MarkFMP: 'firstMeaningfulPaint', |
| MarkLCPCandidate: 'largestContentfulPaint::Candidate', |
| MarkLCPInvalidate: 'largestContentfulPaint::Invalidate', |
| |
| TimeStamp: 'TimeStamp', |
| ConsoleTime: 'ConsoleTime', |
| UserTiming: 'UserTiming', |
| |
| ResourceWillSendRequest: 'ResourceWillSendRequest', |
| ResourceSendRequest: 'ResourceSendRequest', |
| ResourceReceiveResponse: 'ResourceReceiveResponse', |
| ResourceReceivedData: 'ResourceReceivedData', |
| ResourceFinish: 'ResourceFinish', |
| ResourceMarkAsCached: 'ResourceMarkAsCached', |
| |
| 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', |
| StreamingCompileScript: 'v8.parseOnBackground', |
| StreamingCompileScriptWaiting: 'v8.parseOnBackgroundWaiting', |
| StreamingCompileScriptParsing: 'v8.parseOnBackgroundParsing', |
| V8Execute: 'V8.Execute', |
| |
| 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', |
| GCCollectGarbage: 'BlinkGC.AtomicPhase', |
| |
| CryptoDoEncrypt: 'DoEncrypt', |
| CryptoDoEncryptReply: 'DoEncryptReply', |
| CryptoDoDecrypt: 'DoDecrypt', |
| CryptoDoDecryptReply: 'DoDecryptReply', |
| CryptoDoDigest: 'DoDigest', |
| CryptoDoDigestReply: 'DoDigestReply', |
| CryptoDoSign: 'DoSign', |
| CryptoDoSignReply: 'DoSignReply', |
| CryptoDoVerify: 'DoVerify', |
| CryptoDoVerifyReply: 'DoVerifyReply', |
| |
| // CpuProfile is a virtual event created on frontend to support |
| // serialization of CPU Profiles within tracing timeline data. |
| CpuProfile: 'CpuProfile', |
| Profile: 'Profile', |
| |
| AsyncTask: 'AsyncTask', |
| }; |
| |
| TimelineModelImpl.Category = { |
| Console: 'blink.console', |
| UserTiming: 'blink.user_timing', |
| LatencyInfo: 'latencyInfo' |
| }; |
| |
| /** |
| * @enum {string} |
| */ |
| TimelineModelImpl.WarningType = { |
| LongTask: 'LongTask', |
| ForcedStyle: 'ForcedStyle', |
| ForcedLayout: 'ForcedLayout', |
| IdleDeadlineExceeded: 'IdleDeadlineExceeded', |
| LongHandler: 'LongHandler', |
| LongRecurringHandler: 'LongRecurringHandler', |
| V8Deopt: 'V8Deopt' |
| }; |
| |
| TimelineModelImpl.WorkerThreadName = 'DedicatedWorker thread'; |
| TimelineModelImpl.WorkerThreadNameLegacy = 'DedicatedWorker Thread'; |
| TimelineModelImpl.RendererMainThreadName = 'CrRendererMain'; |
| TimelineModelImpl.BrowserMainThreadName = 'CrBrowserMain'; |
| |
| TimelineModelImpl.DevToolsMetadataEvent = { |
| TracingStartedInBrowser: 'TracingStartedInBrowser', |
| TracingStartedInPage: 'TracingStartedInPage', |
| TracingSessionIdForWorker: 'TracingSessionIdForWorker', |
| FrameCommittedInBrowser: 'FrameCommittedInBrowser', |
| ProcessReadyInBrowser: 'ProcessReadyInBrowser', |
| FrameDeletedInBrowser: 'FrameDeletedInBrowser', |
| }; |
| |
| TimelineModelImpl.Thresholds = { |
| LongTask: 200, |
| Handler: 150, |
| RecurringHandler: 50, |
| ForcedLayout: 30, |
| IdleCallbackAddon: 5 |
| }; |
| |
| export class Track { |
| constructor() { |
| this.name = ''; |
| this.type = TrackType.Other; |
| // TODO(dgozman): replace forMainFrame with a list of frames, urls and time ranges. |
| this.forMainFrame = false; |
| this.url = ''; |
| // TODO(dgozman): do not distinguish between sync and async events. |
| /** @type {!Array<!SDK.TracingModel.Event>} */ |
| this.events = []; |
| /** @type {!Array<!SDK.TracingModel.AsyncEvent>} */ |
| this.asyncEvents = []; |
| /** @type {!Array<!SDK.TracingModel.Event>} */ |
| this.tasks = []; |
| this._syncEvents = null; |
| /** @type {?SDK.TracingModel.Thread} */ |
| this.thread = null; |
| } |
| |
| /** |
| * @return {!Array<!SDK.TracingModel.Event>} |
| */ |
| syncEvents() { |
| if (this.events.length) { |
| return this.events; |
| } |
| |
| if (this._syncEvents) { |
| return this._syncEvents; |
| } |
| |
| const stack = []; |
| this._syncEvents = []; |
| for (const event of this.asyncEvents) { |
| const startTime = event.startTime; |
| const endTime = event.endTime; |
| while (stack.length && startTime >= stack.peekLast().endTime) { |
| stack.pop(); |
| } |
| if (stack.length && endTime > stack.peekLast().endTime) { |
| this._syncEvents = []; |
| break; |
| } |
| const syncEvent = new SDK.TracingModel.Event( |
| event.categoriesString, event.name, SDK.TracingModel.Phase.Complete, startTime, event.thread); |
| syncEvent.setEndTime(endTime); |
| syncEvent.addArgs(event.args); |
| this._syncEvents.push(syncEvent); |
| stack.push(syncEvent); |
| } |
| return this._syncEvents; |
| } |
| } |
| |
| /** |
| * @enum {symbol} |
| */ |
| export const TrackType = { |
| MainThread: Symbol('MainThread'), |
| Worker: Symbol('Worker'), |
| Input: Symbol('Input'), |
| Animation: Symbol('Animation'), |
| Timings: Symbol('Timings'), |
| Console: Symbol('Console'), |
| Raster: Symbol('Raster'), |
| GPU: Symbol('GPU'), |
| Other: Symbol('Other'), |
| }; |
| |
| export class PageFrame { |
| /** |
| * @param {!Object} payload |
| */ |
| constructor(payload) { |
| this.frameId = payload['frame']; |
| this.url = payload['url'] || ''; |
| this.name = payload['name']; |
| /** @type {!Array<!PageFrame>} */ |
| this.children = []; |
| /** @type {?PageFrame} */ |
| this.parent = null; |
| /** @type {!Array<!{time: number, processId: number, processPseudoId: ?string, url: string}>} */ |
| this.processes = []; |
| /** @type {?number} */ |
| this.deletedTime = null; |
| // TODO(dgozman): figure this out. |
| // this.ownerNode = target && payload['nodeId'] ? new SDK.DeferredDOMNode(target, payload['nodeId']) : null; |
| this.ownerNode = null; |
| } |
| |
| /** |
| * @param {number} time |
| * @param {!Object} payload |
| */ |
| update(time, payload) { |
| this.url = payload['url'] || ''; |
| this.name = payload['name']; |
| if (payload['processId']) { |
| this.processes.push( |
| {time: time, processId: payload['processId'], processPseudoId: '', url: payload['url'] || ''}); |
| } else { |
| this.processes.push( |
| {time: time, processId: -1, processPseudoId: payload['processPseudoId'], url: payload['url'] || ''}); |
| } |
| } |
| |
| /** |
| * @param {string} processPseudoId |
| * @param {number} processId |
| */ |
| processReady(processPseudoId, processId) { |
| for (const process of this.processes) { |
| if (process.processPseudoId === processPseudoId) { |
| process.processPseudoId = ''; |
| process.processId = processId; |
| } |
| } |
| } |
| |
| /** |
| * @param {!PageFrame} child |
| */ |
| addChild(child) { |
| this.children.push(child); |
| child.parent = this; |
| } |
| } |
| |
| /** |
| * @unrestricted |
| */ |
| export class NetworkRequest { |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| */ |
| constructor(event) { |
| const recordType = RecordType; |
| const isInitial = |
| event.name === recordType.ResourceSendRequest || event.name === recordType.ResourceWillSendRequest; |
| this.startTime = isInitial ? 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; |
| /** @type {number} */ |
| this._transferSize = 0; |
| /** @type {boolean} */ |
| this._maybeDiskCached = false; |
| /** @type {boolean} */ |
| this._memoryCached = false; |
| this.addEvent(event); |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| */ |
| addEvent(event) { |
| this.children.push(event); |
| const recordType = RecordType; |
| // This Math.min is likely because of BUG(chromium:865066). |
| this.startTime = Math.min(this.startTime, event.startTime); |
| const 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; |
| } |
| const encodedDataLength = eventData['encodedDataLength'] || 0; |
| if (event.name === recordType.ResourceMarkAsCached) { |
| // This is a reliable signal for memory caching. |
| this._memoryCached = true; |
| } |
| if (event.name === recordType.ResourceReceiveResponse) { |
| if (eventData['fromCache']) { |
| // See BUG(chromium:998397): back-end over-approximates caching. |
| this._maybeDiskCached = true; |
| } |
| if (eventData['fromServiceWorker']) { |
| this.fromServiceWorker = true; |
| } |
| if (eventData['hasCachedResource']) { |
| this.hasCachedResource = true; |
| } |
| this.encodedDataLength = encodedDataLength; |
| } |
| if (event.name === recordType.ResourceReceivedData) { |
| this.encodedDataLength += encodedDataLength; |
| } |
| if (event.name === recordType.ResourceFinish && encodedDataLength) { |
| this.encodedDataLength = encodedDataLength; |
| // If a ResourceFinish event with an encoded data length is received, |
| // then the resource was not cached; it was fetched before it was |
| // requested, e.g. because it was pushed in this navigation. |
| this._transferSize = encodedDataLength; |
| } |
| const 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; |
| } |
| } |
| |
| /** |
| * Return whether this request was cached. This works around BUG(chromium:998397), |
| * which reports pushed resources, and resources serverd by a service worker as |
| * disk cached. Pushed resources that were not disk cached, however, have a non-zero |
| * `_transferSize`. |
| * @return {boolean} |
| */ |
| cached() { |
| return !!this._memoryCached || (!!this._maybeDiskCached && !this._transferSize && !this.fromServiceWorker); |
| } |
| |
| /** |
| * Return whether this request was served from a memory cache. |
| * @return {boolean} |
| */ |
| memoryCached() { |
| return this._memoryCached; |
| } |
| |
| /** |
| * Get the timing information for this request. If the request was cached, |
| * the timing refers to the original (uncached) load, and should not be used. |
| * @return {!{sendStartTime: number, headersEndTime: number}} |
| */ |
| getSendReceiveTiming() { |
| if (this.cached() || !this.timing) { |
| // If the request is served from cache, the timing refers to the original |
| // resource load, and should not be used. |
| return {sendStartTime: this.startTime, headersEndTime: this.startTime}; |
| } |
| const requestTime = this.timing.requestTime * 1000; |
| const sendStartTime = requestTime + this.timing.sendStart; |
| const headersEndTime = requestTime + this.timing.receiveHeadersEnd; |
| return {sendStartTime, headersEndTime}; |
| } |
| |
| /** |
| * Get the start time of this request, i.e. the time when the browser or |
| * renderer queued this request. There are two cases where request time is |
| * earlier than `startTime`: (1) if the request is served from cache, because |
| * it refers to the original load of the resource. (2) if the request was |
| * initiated by the browser instead of the renderer. Only in case (2) the |
| * the request time must be used instead of the start time to work around |
| * BUG(chromium:865066). |
| * @return {number} |
| */ |
| getStartTime() { |
| return Math.min(this.startTime, !this.cached() && this.timing && this.timing.requestTime * 1000 || Infinity); |
| } |
| |
| /** |
| * Returns the time where the earliest event belonging to this request starts. |
| * This differs from `getStartTime()` if a previous HTTP/2 request pushed the |
| * resource proactively: Then `beginTime()` refers to the time the push was received. |
| * @return {number} |
| */ |
| beginTime() { |
| // `pushStart` is referring to the original push if the request was cached (i.e. in |
| // general not the most recent push), and should hence only be used for requests that were not cached. |
| return Math.min(this.getStartTime(), !this.cached() && this.timing && this.timing.pushStart * 1000 || Infinity); |
| } |
| } |
| |
| /** |
| * @unrestricted |
| */ |
| export class InvalidationTrackingEvent { |
| /** |
| * @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; |
| |
| const 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.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 === RecordType.LayoutInvalidationTracking) { |
| this.cause.reason = 'Layout forced'; |
| } |
| } |
| } |
| |
| export class InvalidationTracker { |
| 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<!InvalidationTrackingEvent>} |
| */ |
| static invalidationEventsFor(event) { |
| return event[InvalidationTracker._invalidationTrackingEventsSymbol] || null; |
| } |
| |
| /** |
| * @param {!InvalidationTrackingEvent} invalidation |
| */ |
| addInvalidation(invalidation) { |
| this._startNewFrameIfNeeded(); |
| |
| if (!invalidation.nodeId) { |
| console.error('Invalidation lacks node information.'); |
| console.error(invalidation); |
| return; |
| } |
| |
| const recordTypes = RecordType; |
| |
| // 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. |
| const styleRecalcInvalidation = |
| (invalidation.type === recordTypes.ScheduleStyleInvalidationTracking || |
| invalidation.type === recordTypes.StyleInvalidatorInvalidationTracking || |
| invalidation.type === recordTypes.StyleRecalcInvalidationTracking); |
| if (styleRecalcInvalidation) { |
| const 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; |
| const types = [ |
| RecordType.ScheduleStyleInvalidationTracking, RecordType.StyleInvalidatorInvalidationTracking, |
| RecordType.StyleRecalcInvalidationTracking |
| ]; |
| for (const invalidation of this._invalidationsOfTypes(types)) { |
| this._associateWithLastRecalcStyleEvent(invalidation); |
| } |
| } |
| |
| /** |
| * @param {!InvalidationTrackingEvent} invalidation |
| */ |
| _associateWithLastRecalcStyleEvent(invalidation) { |
| if (invalidation.linkedRecalcStyleEvent) { |
| return; |
| } |
| |
| const recordTypes = RecordType; |
| const 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 {!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(styleInvalidatorInvalidation); |
| return; |
| } |
| for (let i = 0; i < styleInvalidatorInvalidation.invalidationList.length; i++) { |
| const setId = styleInvalidatorInvalidation.invalidationList[i]['id']; |
| let lastScheduleStyleRecalculation; |
| const nodeInvalidations = this._invalidationsByNodeId[styleInvalidatorInvalidation.nodeId] || []; |
| for (let j = 0; j < nodeInvalidations.length; j++) { |
| const invalidation = nodeInvalidations[j]; |
| if (invalidation.frame !== frameId || invalidation.invalidationSet !== setId || |
| invalidation.type !== 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 {!InvalidationTrackingEvent} styleInvalidatorInvalidation |
| */ |
| _addSyntheticStyleRecalcInvalidation(baseEvent, styleInvalidatorInvalidation) { |
| const invalidation = new InvalidationTrackingEvent(baseEvent); |
| invalidation.type = 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) { |
| const layoutFrameId = layoutEvent.args['beginData']['frame']; |
| for (const invalidation of this._invalidationsOfTypes([RecordType.LayoutInvalidationTracking])) { |
| if (invalidation.linkedLayoutEvent) { |
| continue; |
| } |
| this._addInvalidationToEvent(layoutEvent, layoutFrameId, invalidation); |
| invalidation.linkedLayoutEvent = true; |
| } |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} paintEvent |
| */ |
| didPaint(paintEvent) { |
| this._didPaint = true; |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| * @param {number} eventFrameId |
| * @param {!InvalidationTrackingEvent} invalidation |
| */ |
| _addInvalidationToEvent(event, eventFrameId, invalidation) { |
| if (eventFrameId !== invalidation.frame) { |
| return; |
| } |
| if (!event[InvalidationTracker._invalidationTrackingEventsSymbol]) { |
| event[InvalidationTracker._invalidationTrackingEventsSymbol] = [invalidation]; |
| } else { |
| event[InvalidationTracker._invalidationTrackingEventsSymbol].push(invalidation); |
| } |
| } |
| |
| /** |
| * @param {!Array.<string>=} types |
| * @return {!Generator<!InvalidationTrackingEvent>} |
| */ |
| _invalidationsOfTypes(types) { |
| const invalidations = this._invalidations; |
| if (!types) { |
| types = Object.keys(invalidations); |
| } |
| function* generator() { |
| for (let i = 0; i < types.length; ++i) { |
| const invalidationList = invalidations[types[i]] || []; |
| for (let j = 0; j < invalidationList.length; ++j) { |
| yield invalidationList[j]; |
| } |
| } |
| } |
| return generator(); |
| } |
| |
| _startNewFrameIfNeeded() { |
| if (!this._didPaint) { |
| return; |
| } |
| |
| this._initializePerFrameState(); |
| } |
| |
| _initializePerFrameState() { |
| /** @type {!Object.<string, !Array.<!InvalidationTrackingEvent>>} */ |
| this._invalidations = {}; |
| /** @type {!Object.<number, !Array.<!InvalidationTrackingEvent>>} */ |
| this._invalidationsByNodeId = {}; |
| |
| this._lastRecalcStyle = null; |
| this._lastPaintWithLayer = null; |
| this._didPaint = false; |
| } |
| } |
| |
| InvalidationTracker._invalidationTrackingEventsSymbol = Symbol('invalidationTrackingEvents'); |
| |
| /** |
| * @unrestricted |
| */ |
| export class TimelineAsyncEventTracker { |
| constructor() { |
| TimelineAsyncEventTracker._initialize(); |
| /** @type {!Map<!RecordType, !Map<string, !SDK.TracingModel.Event>>} */ |
| this._initiatorByType = new Map(); |
| for (const initiator of TimelineAsyncEventTracker._asyncEvents.keys()) { |
| this._initiatorByType.set(initiator, new Map()); |
| } |
| } |
| |
| static _initialize() { |
| if (TimelineAsyncEventTracker._asyncEvents) { |
| return; |
| } |
| const events = new Map(); |
| let type = RecordType; |
| |
| events.set(type.TimerInstall, {causes: [type.TimerFire], joinBy: 'timerId'}); |
| events.set(type.ResourceSendRequest, { |
| causes: [type.ResourceMarkAsCached, 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' |
| }); |
| |
| TimelineAsyncEventTracker._asyncEvents = events; |
| /** @type {!Map<!RecordType, !RecordType>} */ |
| TimelineAsyncEventTracker._typeToInitiator = new Map(); |
| for (const entry of events) { |
| const types = entry[1].causes; |
| for (type of types) { |
| TimelineAsyncEventTracker._typeToInitiator.set(type, entry[0]); |
| } |
| } |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| */ |
| processEvent(event) { |
| let initiatorType = TimelineAsyncEventTracker._typeToInitiator.get( |
| /** @type {!RecordType} */ (event.name)); |
| const isInitiator = !initiatorType; |
| if (!initiatorType) { |
| initiatorType = /** @type {!RecordType} */ (event.name); |
| } |
| const initiatorInfo = TimelineAsyncEventTracker._asyncEvents.get(initiatorType); |
| if (!initiatorInfo) { |
| return; |
| } |
| const id = TimelineModelImpl.globalEventId(event, initiatorInfo.joinBy); |
| if (!id) { |
| return; |
| } |
| /** @type {!Map<string, !SDK.TracingModel.Event>|undefined} */ |
| const initiatorMap = this._initiatorByType.get(initiatorType); |
| if (isInitiator) { |
| initiatorMap.set(id, event); |
| return; |
| } |
| const initiator = initiatorMap.get(id) || null; |
| const timelineData = TimelineData.forEvent(event); |
| timelineData.setInitiator(initiator); |
| if (!timelineData.frameId && initiator) { |
| timelineData.frameId = TimelineModelImpl.eventFrameId(initiator); |
| } |
| } |
| } |
| |
| export class TimelineData { |
| 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; |
| } |
| const initiatorURL = TimelineData.forEvent(initiator).url; |
| if (initiatorURL) { |
| this.url = initiatorURL; |
| } |
| } |
| |
| /** |
| * @return {?SDK.TracingModel.Event} |
| */ |
| initiator() { |
| return this._initiator; |
| } |
| |
| /** |
| * @return {?Protocol.Runtime.CallFrame} |
| */ |
| topFrame() { |
| const stackTrace = this.stackTraceForSelfOrInitiator(); |
| return stackTrace && stackTrace[0] || null; |
| } |
| |
| /** |
| * @return {?Array<!Protocol.Runtime.CallFrame>} |
| */ |
| stackTraceForSelfOrInitiator() { |
| return this.stackTrace || (this._initiator && TimelineData.forEvent(this._initiator).stackTrace); |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| * @return {!TimelineData} |
| */ |
| static forEvent(event) { |
| let data = event[TimelineData._symbol]; |
| if (!data) { |
| data = new TimelineData(); |
| event[TimelineData._symbol] = data; |
| } |
| return data; |
| } |
| } |
| |
| TimelineData._symbol = Symbol('timelineData'); |
| |
| /* Legacy exported object */ |
| self.TimelineModel = self.TimelineModel || {}; |
| |
| /* Legacy exported object */ |
| TimelineModel = TimelineModel || {}; |
| |
| /** @constructor */ |
| TimelineModel.TimelineModel = TimelineModelImpl; |
| |
| /** @constructor */ |
| TimelineModel.TimelineModel.Track = Track; |
| |
| /** @enum {symbol} */ |
| TimelineModel.TimelineModel.TrackType = TrackType; |
| |
| /** @enum {string} */ |
| TimelineModel.TimelineModel.RecordType = RecordType; |
| |
| /** @constructor */ |
| TimelineModel.TimelineModel.PageFrame = PageFrame; |
| |
| /** @constructor */ |
| TimelineModel.TimelineModel.NetworkRequest = NetworkRequest; |
| |
| /** @constructor */ |
| TimelineModel.InvalidationTrackingEvent = InvalidationTrackingEvent; |
| |
| /** @constructor */ |
| TimelineModel.InvalidationTracker = InvalidationTracker; |
| |
| /** @constructor */ |
| TimelineModel.TimelineAsyncEventTracker = TimelineAsyncEventTracker; |
| |
| /** @constructor */ |
| TimelineModel.TimelineData = TimelineData; |
| |
| /** @typedef {{reason: string, stackTrace: ?Array<!Protocol.Runtime.CallFrame>}} */ |
| TimelineModel.InvalidationCause; |
| |
| /** @typedef {!{page: !Array<!SDK.TracingModel.Event>, workers: !Array<!SDK.TracingModel.Event>}} */ |
| TimelineModel.TimelineModel.MetadataEvents; |