UNPKG

chrome-devtools-frontend

Version:
1,377 lines (1,276 loc) 89.3 kB
/* * 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. */ import * as Common from '../common/common.js'; import * as i18n from '../i18n/i18n.js'; import * as Platform from '../platform/platform.js'; import * as Root from '../root/root.js'; import * as SDK from '../sdk/sdk.js'; import {TimelineJSProfileProcessor} from './TimelineJSProfile.js'; export const UIStrings = { /** *@description Text for the name of a thread of the page *@example {1} PH1 */ threadS: 'Thread {PH1}', /** *@description Title of a worker in the timeline flame chart of the Performance panel *@example {https://google.com} PH1 */ workerS: '`Worker` — {PH1}', /** *@description Title of a worker in the timeline flame chart of the Performance panel */ dedicatedWorker: 'Dedicated `Worker`', /** *@description Title of a worker in the timeline flame chart of the Performance panel *@example {FormatterWorker} PH1 *@example {https://google.com} PH2 */ workerSS: '`Worker`: {PH1} — {PH2}', }; const str_ = i18n.i18n.registerUIStrings('timeline_model/TimelineModel.js', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export class TimelineModelImpl { constructor() { // The following fields are set by a call to _reset(). /** @type {boolean} */ this._isGenericTrace; /** @type {!Array<!Track>} */ this._tracks; /** @type {!Map<!TrackType, !Track>} */ this._namedTracks; /** @type {!Array<!SDK.TracingModel.Event>} */ this._inspectedTargetEvents; /** @type {!Array<!SDK.TracingModel.Event>} */ this._timeMarkerEvents; /** @type {?string} */ this._sessionId; /** @type {?number} */ this._mainFrameNodeId; /** @type {!Map<string, !PageFrame>} */ this._pageFrames; /** @type {!Array<!SDK.CPUProfileDataModel.CPUProfileDataModel>} */ this._cpuProfiles; /** @type WeakMap<SDK.TracingModel.Thread, string> */ this._workerIdByThread; /** @type {!Map<string, !SDK.TracingModel.Event>} */ this._requestsFromBrowser; /** @type {!PageFrame} */ this._mainFrame; this._minimumRecordTime = 0; this._maximumRecordTime = 0; this._totalBlockingTime = 0; this._estimatedTotalBlockingTime = 0; // The following are set by a call to resetProcessingState(). /** @type {!TimelineAsyncEventTracker} */ this._asyncEventTracker; /** @type {!InvalidationTracker} */ this._invalidationTracker; /** @type {!Object.<string, ?SDK.TracingModel.Event>} */ this._layoutInvalidate; /** @type {!Object.<string, !SDK.TracingModel.Event>} */ this._lastScheduleStyleRecalculation; /** @type {!Object.<string, !SDK.TracingModel.Event>} */ this._paintImageEventByPixelRefId; /** @type {!Object.<string, !SDK.TracingModel.Event>} */ this._lastPaintForLayer; /** @type {?SDK.TracingModel.Event} */ this._lastRecalculateStylesEvent; /** @type {?SDK.TracingModel.Event} */ this._currentScriptEvent; /** @type {!Array<!SDK.TracingModel.Event>} */ this._eventStack; /** @type {!Set<string>} */ this._knownInputEvents; /** @type {boolean} */ this._browserFrameTracking; /** @type {boolean} */ this._persistentIds; /** @type {*} */ this._legacyCurrentPage; this._reset(); this._resetProcessingState(); /** @type {!Array<!SDK.TracingModel.Event>} */ this._currentTaskLayoutAndRecalcEvents = []; /** @type {?SDK.TracingModel.TracingModel} */ this._tracingModel = null; } /** * @param {!Array<!SDK.TracingModel.Event>} events * @param {function(!SDK.TracingModel.Event): void} onStartEvent * @param {function(!SDK.TracingModel.Event): void} 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; /** @type {!Array<!SDK.TracingModel.Event>} */ 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.TracingModel.isAsyncPhase(e.phase) || SDK.TracingModel.TracingModel.isFlowPhase(e.phase)) { continue; } let last = stack[stack.length - 1]; while (last && last.endTime !== undefined && last.endTime <= e.startTime) { stack.pop(); onEndEvent(last); last = stack[stack.length - 1]; } if (filter && !filter(e)) { continue; } if (e.duration) { onStartEvent(e); stack.push(e); } else { onInstantEvent && onInstantEvent(e, stack[stack.length - 1] || null); } } while (stack.length) { const last = stack.pop(); if (last) { onEndEvent(last); } } } /** * @param {!Array<!SDK.TracingModel.Event>} events * @param {number} time */ static _topLevelEventEndingAfter(events, time) { let index = Platform.ArrayUtilities.upperBound(events, time, (time, event) => time - event.startTime) - 1; while (index > 0 && !SDK.TracingModel.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: return Boolean(this._mainFrame) && event.args.frame === this._mainFrame.frameId && Boolean(event.args.data); case recordTypes.MarkDOMContent: case recordTypes.MarkLoad: case recordTypes.MarkLCPCandidate: case recordTypes.MarkLCPInvalidate: return Boolean(event.args['data']['isMainFrame']); default: return false; } } /** * @param {!SDK.TracingModel.Event} event * @return {boolean} */ isInteractiveTimeEvent(event) { return event.name === RecordType.InteractiveTime; } /** * @param {!SDK.TracingModel.Event} event * @return {boolean} */ isLayoutShiftEvent(event) { return event.name === RecordType.LayoutShift; } /** * @param {!SDK.TracingModel.Event} event * @return {boolean} */ isUserTimingEvent(event) { return event.categoriesString === TimelineModelImpl.Category.UserTiming; } /** * @param {!SDK.TracingModel.Event} event * @return {boolean} */ isParseHTMLEvent(event) { return event.name === RecordType.ParseHTML; } /** * @param {!SDK.TracingModel.Event} event * @return {boolean} */ isLCPCandidateEvent(event) { return event.name === RecordType.MarkLCPCandidate && Boolean(event.args['data']['isMainFrame']); } /** * @param {!SDK.TracingModel.Event} event * @return {boolean} */ isLCPInvalidateEvent(event) { return event.name === RecordType.MarkLCPInvalidate && Boolean(event.args['data']['isMainFrame']); } /** * @param {!SDK.TracingModel.Event} event * @return {boolean} */ isFCPEvent(event) { return event.name === RecordType.MarkFCP && Boolean(this._mainFrame) && event.args['frame'] === this._mainFrame.frameId; } /** * @param {!SDK.TracingModel.Event} event * @return {boolean} */ isLongRunningTask(event) { return event.name === RecordType.Task && TimelineData.forEvent(event).warning === TimelineModelImpl.WarningType.LongTask; } /** * @param {!SDK.TracingModel.Event} event * @return {boolean} */ isNavigationStartEvent(event) { return event.name === RecordType.NavigationStart; } /** * @param {!SDK.TracingModel.Event} event * @return {boolean} */ isMainFrameNavigationStartEvent(event) { return this.isNavigationStartEvent(event) && event.args['data']['isLoadingMainFrame'] && event.args['data']['documentLoaderURL']; } /** * @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.CPUProfileDataModel>} */ cpuProfiles() { return this._cpuProfiles; } /** * @return {{time: number, estimated: boolean}} */ totalBlockingTime() { if (this._totalBlockingTime === -1) { return {time: this._estimatedTotalBlockingTime, estimated: true}; } return {time: this._totalBlockingTime, estimated: false}; } /** * @param {!SDK.TracingModel.Event} event * @return {?SDK.SDKModel.Target} */ targetByEvent(event) { // FIXME: Consider returning null for loaded traces. const workerId = this._workerIdByThread.get(event.thread); const mainTarget = SDK.SDKModel.TargetManager.instance().mainTarget(); return workerId ? SDK.SDKModel.TargetManager.instance().targetById(workerId) : mainTarget; } /** * @return {!Map<string, !SDK.TracingModel.Event>} */ navStartTimes() { if (!this._tracingModel) { return new Map(); } return this._tracingModel.navStartTimes(); } /** * @param {!SDK.TracingModel.TracingModel} tracingModel */ setEvents(tracingModel) { this._reset(); this._resetProcessingState(); this._tracingModel = tracingModel; this._minimumRecordTime = tracingModel.minimumRecordTime(); this._maximumRecordTime = tracingModel.maximumRecordTime(); // Remove LayoutShift events from the main thread list of events because they are // represented in the experience track. This is done prior to the main thread being processed for its own events. const layoutShiftEvents = []; for (const process of tracingModel.sortedProcesses()) { if (process.name() !== 'Renderer') { continue; } for (const thread of process.sortedThreads()) { const shifts = thread.removeEventsByName(RecordType.LayoutShift); layoutShiftEvents.push(...shifts); } } 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._buildLoadingEvents(tracingModel, layoutShiftEvents); this._resetProcessingState(); } /** * @param {!SDK.TracingModel.TracingModel} tracingModel */ _processGenericTrace(tracingModel) { let browserMainThread = SDK.TracingModel.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} tracingModel * @param {!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 Boolean(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, Boolean(workerUrl), true, workerUrl); } startTime = endTime; } } /** * @param {!SDK.TracingModel.TracingModel} tracingModel */ _processThreadsForBrowserFrames(tracingModel) { /** @type {!Map<number, !Array<{from: number, to: number, main: boolean, url: string}>>} */ 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) { const last = ranges[ranges.length - 1]; if (!last || item.from > last.to) { ranges.push({from: item.from, to: item.to}); } else { last.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 Boolean(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} tracingModel * @return {?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; } /** @type {!Array<!PageFrame>} */ 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.Console.instance().error( 'Timeline recording was started in more than one page simultaneously. Session id mismatch: ' + this._sessionId + ' and ' + [...mismatchingIds] + '.'); } return result; } /** * @param {!SDK.TracingModel.TracingModel} tracingModel */ _processSyncBrowserEvents(tracingModel) { const browserMain = SDK.TracingModel.TracingModel.browserMainThread(tracingModel); if (browserMain) { browserMain.events().forEach(this._processBrowserEvent, this); } } /** * @param {!SDK.TracingModel.TracingModel} tracingModel */ _processAsyncBrowserEvents(tracingModel) { const browserMain = SDK.TracingModel.TracingModel.browserMainThread(tracingModel); if (browserMain) { this._processAsyncEvents(browserMain, [{from: 0, to: Infinity}]); } } /** * @param {!SDK.TracingModel.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); } /** * @param {!SDK.TracingModel.TracingModel} tracingModel * @param {!Array<!SDK.TracingModel.Event>} events */ _buildLoadingEvents(tracingModel, events) { const thread = tracingModel.threadByName('Renderer', 'CrRendererMain'); if (!thread) { return; } const experienceCategory = 'experience'; const track = this._ensureNamedTrack(TrackType.Experience); track.thread = thread; track.events = events; // Even though the event comes from 'loading', in order to color it differently we // rename its category. for (const trackEvent of track.events) { trackEvent.categoriesString = experienceCategory; if (trackEvent.name === RecordType.LayoutShift) { const eventData = trackEvent.args['data'] || trackEvent.args['beginData'] || {}; const timelineData = TimelineData.forEvent(trackEvent); timelineData.backendNodeId = eventData['impacted_nodes'] && eventData['impacted_nodes'].length > 0 ? eventData['impacted_nodes'][0]['node_id'] : 0; } } } _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 = []; this._knownInputEvents = new Set(); this._browserFrameTracking = false; this._persistentIds = false; this._legacyCurrentPage = null; } /** * @param {!SDK.TracingModel.TracingModel} tracingModel * @param {!SDK.TracingModel.Thread} thread * @return {?SDK.CPUProfileDataModel.CPUProfileDataModel} */ _extractCpuProfile(tracingModel, thread) { const events = thread.events(); let cpuProfile; let target = null; // Check for legacy CpuProfile event format first. /** @type {(SDK.TracingModel.Event|undefined)} */ let cpuProfileEvent = events[events.length - 1]; 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.Console.instance().error('Invalid CPU profile format.'); return null; } // This overlaps with the Protocol.Profiler.Profile type, but adds // the lines array. cpuProfile = /** @type {*} */ ({ // Do not use |cpuProfileEvent.args['data']['startTime']| as it is in // CLOCK_MONOTONIC domain, but use |profileEvent.startTime| // (|ts| in the trace event) which has been translated to // Perfetto's clock domain. // // |cpuProfileEvent.startTime| has been converted to milliseconds // when the Event was loaded but |cpuProfile.timeDeltas| are // expressed in microseconds. startTime: cpuProfileEvent.startTime * 1000, endTime: 0, nodes: [], samples: [], timeDeltas: [], lines: [] }); for (const profileEvent of profileGroup.children) { const eventData = profileEvent.args['data']; if ('startTime' in eventData) { // Do not use |eventData['startTime']| as it is in CLOCK_MONOTONIC domain, // but use |profileEvent.startTime| (|ts| in the trace event) which has // been translated to Perfetto's clock domain. // // Also convert from ms to us. cpuProfile.startTime = profileEvent.startTime * 1000; } if ('endTime' in eventData) { // Do not use |eventData['endTime']| as it is in CLOCK_MONOTONIC domain, // but use |profileEvent.startTime| (|ts| in the trace event) which has // been translated to Perfetto's clock domain. // // Despite its name, |profileEvent.startTime| was recorded right after // |eventData['endTime']| within v8 and is a reasonable substitute. // // Also convert from ms to us. cpuProfile.endTime = profileEvent.startTime * 1000; } const nodesAndSamples = eventData['cpuProfile'] || {}; const samples = nodesAndSamples['samples'] || []; const lines = eventData['lines'] || Array(samples.length).fill(0); cpuProfile.nodes.push(...(nodesAndSamples['nodes'] || [])); cpuProfile.lines.push(...lines); if (cpuProfile.samples) { cpuProfile.samples.push(...samples); } if (cpuProfile.timeDeltas) { cpuProfile.timeDeltas.push(...(eventData['timeDeltas'] || [])); } if (cpuProfile.samples && cpuProfile.timeDeltas && cpuProfile.samples.length !== cpuProfile.timeDeltas.length) { Common.Console.Console.instance().error('Failed to parse CPU profile.'); return null; } } if (!cpuProfile.endTime && cpuProfile.timeDeltas) { /** @type {!Array<number>} */ const timeDeltas = cpuProfile.timeDeltas; cpuProfile.endTime = timeDeltas.reduce((x, y) => x + y, cpuProfile.startTime); } } try { const profile = /** @type {!Protocol.Profiler.Profile} */ (cpuProfile); const jsProfileModel = new SDK.CPUProfileDataModel.CPUProfileDataModel(profile, target); this._cpuProfiles.push(jsProfileModel); return jsProfileModel; } catch (e) { Common.Console.Console.instance().error('Failed to parse CPU profile.'); } return null; } /** * @param {!SDK.TracingModel.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 ? TimelineJSProfileProcessor.generateTracingEventsFromCpuProfile(jsProfileModel, thread) : null; if (jsSamples && jsSamples.length) { events = Platform.ArrayUtilities.mergeOrdered(events, jsSamples, SDK.TracingModel.Event.orderedCompareStartTime); } if (jsSamples || events.some(e => e.name === RecordType.JSSample)) { const jsFrameEvents = TimelineJSProfileProcessor.generateJSFrameEvents(events, { showAllEvents: Root.Runtime.experiments.isEnabled('timelineShowAllEvents'), showRuntimeCallStats: Root.Runtime.experiments.isEnabled('timelineV8RuntimeCallStats'), showNativeFunctions: Common.Settings.Settings.instance().moduleSetting('showNativeFunctionsInJSProfile').get(), }); if (jsFrameEvents && jsFrameEvents.length) { events = Platform.ArrayUtilities.mergeOrdered(jsFrameEvents, events, SDK.TracingModel.Event.orderedCompareStartTime); } } return events; } /** * @param {!SDK.TracingModel.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() || i18nString(UIStrings.threadS, {PH1: thread.id()}); track.type = TrackType.Other; track.thread = thread; if (isMainThread) { track.type = TrackType.MainThread; track.url = url || ''; track.forMainFrame = forMainFrame; } else if (isWorker) { track.type = TrackType.Worker; track.url = url || ''; track.name = track.url ? i18nString(UIStrings.workerS, {PH1: track.url}) : i18nString(UIStrings.dedicatedWorker); } 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; // Get the worker name from the target. if (isWorker) { const cpuProfileEvent = events.find(event => event.name === RecordType.Profile); if (cpuProfileEvent) { const target = this.targetByEvent(cpuProfileEvent); if (target) { track.name = i18nString(UIStrings.workerSS, {PH1: target.name(), PH2: track.url}); } } } for (const range of ranges) { let i = Platform.ArrayUtilities.lowerBound(events, range.from, (time, event) => time - event.startTime); for (; i < events.length; i++) { const event = events[i]; if (event.startTime >= range.to) { break; } // There may be several TTI events, only take the first one. if (this.isInteractiveTimeEvent(event) && this._totalBlockingTime === -1) { this._totalBlockingTime = event.args['args']['total_blocking_time_ms']; } const isLongRunningTask = event.name === RecordType.Task && event.duration && event.duration > 50; if (isMainThread && isLongRunningTask && event.duration) { // We only track main thread events that are over 50ms, and the amount of time in the // event (over 50ms) is what constitutes the blocking time. An event of 70ms, therefore, // contributes 20ms to TBT. this._estimatedTotalBlockingTime += event.duration - 50; } let last = eventStack[eventStack.length - 1]; while (last && last.endTime !== undefined && last.endTime <= event.startTime) { eventStack.pop(); last = eventStack[eventStack.length - 1]; } if (!this._processEvent(event)) { continue; } if (!SDK.TracingModel.TracingModel.isAsyncPhase(event.phase) && event.duration) { if (eventStack.length) { const parent = eventStack[eventStack.length - 1]; if (parent) { 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 = Platform.ArrayUtilities.lowerBound(asyncEvents, 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[asyncEvent.steps.length - 1]; if (!lastStep) { throw new Error('AsyncEvent.steps access is out of bounds.'); } // FIXME: fix event termination on the back-end instead. if (lastStep.phase !== SDK.TracingModel.Phase.AsyncEnd) { continue; } const data = lastStep.args['data']; asyncEvent.causedFrame = Boolean(data && data['INPUT_EVENT_LATENCY_RENDERER_SWAP_COMPONENT']); if (asyncEvent.hasCategory(TimelineModelImpl.Category.LatencyInfo)) { if (lastStep.id && !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 = Platform.ArrayUtilities.mergeOrdered(track.asyncEvents, 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) => { return event.duration === undefined ? time : 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) { if (this._currentScriptEvent.endTime !== undefined && 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); const last = eventStack[eventStack.length - 1]; if (!pageFrameId && last) { pageFrameId = TimelineData.forEvent(last).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[eventStack.length - 1] || 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. /** @type {?SDK.TracingModel.Event} */ let layoutInitator = event; const frameId = eventData['frame']; if (!this._layoutInvalidate[frameId] && this._lastRecalculateStylesEvent && this._lastRecalculateStylesEvent.endTime !== undefined && 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 !== undefined && event.duration > TimelineModelImpl.Thresholds.LongTask) { timelineData.warning = TimelineModelImpl.WarningType.LongTask; } break; } case recordTypes.EventDispatch: { if (event.duration !== undefined && event.duration > TimelineModelImpl.Thresholds.RecurringHandler) { timelineData.warning = TimelineModelImpl.WarningType.LongHandler; } break; } case recordTypes.TimerFire: case recordTypes.FireAnimationFrame: { if (event.duration !== undefined && event.duration > TimelineModelImpl.Thresholds.RecurringHandler) { timelineData.warning = TimelineModelImpl.WarningType.LongRecurringHandler; } break; } // @ts-ignore fallthrough intended. 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']; } } case recordTypes.EvaluateScript: // @ts-ignore fallthrough intended. case recordTypes.CompileScript: { if (typeof eventData['lineNumber'] === 'number') { --eventData['lineNumber']; } if (typeof eventData['columnNumber'] === 'number') { --eventData['columnNumber']; } } 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 = Boolean(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) { const frame = this._pageFrames.get(frameId); if (frame) { this._mainFrame = frame; } } break; } case recordTypes.FireIdleCallback: { if (event.duration !== undefined && 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 && event.bind_id) { 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']; /** @type{!Array<*>} */ 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