UNPKG

chrome-devtools-frontend

Version:
1,247 lines (1,141 loc) 96.1 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. */ // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) /* eslint-disable @typescript-eslint/no-explicit-any */ import * as Common from '../../core/common/common.js'; import * as i18n from '../../core/i18n/i18n.js'; import * as Platform from '../../core/platform/platform.js'; import * as Root from '../../core/root/root.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as TraceEngine from '../trace/trace.js'; import type * as Protocol from '../../generated/protocol.js'; import {TimelineJSProfileProcessor} from './TimelineJSProfile.js'; 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}', /** *@description Title of a bidder auction worklet with known URL in the timeline flame chart of the Performance panel *@example {https://google.com} PH1 */ bidderWorkletS: 'Bidder Worklet — {PH1}', /** *@description Title of a seller auction worklet with known URL in the timeline flame chart of the Performance panel *@example {https://google.com} PH1 */ sellerWorkletS: 'Seller Worklet — {PH1}', /** *@description Title of an auction worklet with known URL in the timeline flame chart of the Performance panel *@example {https://google.com} PH1 */ unknownWorkletS: 'Auction Worklet — {PH1}', /** *@description Title of a bidder auction worklet in the timeline flame chart of the Performance panel */ bidderWorklet: 'Bidder Worklet', /** *@description Title of a seller auction worklet in the timeline flame chart of the Performance panel */ sellerWorklet: 'Seller Worklet', /** *@description Title of an auction worklet in the timeline flame chart of the Performance panel */ unknownWorklet: 'Auction Worklet', /** *@description Title of control thread of a service process for an auction worklet in the timeline flame chart of the Performance panel */ workletService: 'Auction Worklet Service', /** *@description Title of control thread of a service process for an auction worklet with known URL in the timeline flame chart of the Performance panel * @example {https://google.com} PH1 */ workletServiceS: 'Auction Worklet Service — {PH1}', }; const str_ = i18n.i18n.registerUIStrings('models/timeline_model/TimelineModel.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export class TimelineModelImpl { private isGenericTraceInternal!: boolean; private tracksInternal!: Track[]; private namedTracks!: Map<TrackType, Track>; private inspectedTargetEventsInternal!: SDK.TracingModel.Event[]; private timeMarkerEventsInternal!: SDK.TracingModel.Event[]; private sessionId!: string|null; private mainFrameNodeId!: number|null; private pageFrames!: Map<Protocol.Page.FrameId, PageFrame>; private auctionWorklets!: Map<string, AuctionWorklet>; private cpuProfilesInternal!: SDK.CPUProfileDataModel.CPUProfileDataModel[]; private workerIdByThread!: WeakMap<SDK.TracingModel.Thread, string>; private requestsFromBrowser!: Map<string, SDK.TracingModel.Event>; private mainFrame!: PageFrame; private minimumRecordTimeInternal: number; private maximumRecordTimeInternal: number; private totalBlockingTimeInternal: number; private estimatedTotalBlockingTime: number; private asyncEventTracker!: TimelineAsyncEventTracker; private invalidationTracker!: InvalidationTracker; private layoutInvalidate!: { [x: string]: SDK.TracingModel.Event|null, }; private lastScheduleStyleRecalculation!: { [x: string]: SDK.TracingModel.Event, }; private paintImageEventByPixelRefId!: { [x: string]: SDK.TracingModel.Event, }; private lastPaintForLayer!: { [x: string]: SDK.TracingModel.Event, }; private lastRecalculateStylesEvent!: SDK.TracingModel.Event|null; private currentScriptEvent!: SDK.TracingModel.Event|null; private eventStack!: SDK.TracingModel.Event[]; private browserFrameTracking!: boolean; private persistentIds!: boolean; private legacyCurrentPage!: any; private currentTaskLayoutAndRecalcEvents: SDK.TracingModel.Event[]; private tracingModelInternal: SDK.TracingModel.TracingModel|null; private mainFrameLayerTreeId?: any; #isFreshRecording = false; constructor() { this.minimumRecordTimeInternal = 0; this.maximumRecordTimeInternal = 0; this.totalBlockingTimeInternal = 0; this.estimatedTotalBlockingTime = 0; this.reset(); this.resetProcessingState(); this.currentTaskLayoutAndRecalcEvents = []; this.tracingModelInternal = null; } /** * Iterates events in a tree hierarchically, from top to bottom, * calling back on every event's start and end in the order * dictated by the corresponding timestamp. * * Events are assumed to be in ascendent order by timestamp. * * For example, given this tree, the following callbacks * are expected to be made in the following order * |---------------A---------------| * |------B------||-------D------| * |---C---| * * 1. Start A * 3. Start B * 4. Start C * 5. End C * 6. End B * 7. Start D * 8. End D * 9. End A * * By default, async events are filtered. This behaviour can be * overriden making use of the filterAsyncEvents parameter. */ static forEachEvent( events: SDK.TracingModel.CompatibleTraceEvent[], onStartEvent: (arg0: SDK.TracingModel.CompatibleTraceEvent) => void, onEndEvent: (arg0: SDK.TracingModel.CompatibleTraceEvent) => void, onInstantEvent?: ((arg0: SDK.TracingModel.CompatibleTraceEvent, arg1: SDK.TracingModel.CompatibleTraceEvent|null) => void), startTime?: number, endTime?: number, filter?: ((arg0: SDK.TracingModel.CompatibleTraceEvent) => boolean), ignoreAsyncEvents = true): void { startTime = startTime || 0; endTime = endTime || Infinity; const stack: SDK.TracingModel.CompatibleTraceEvent[] = []; const startEvent = TimelineModelImpl.topLevelEventEndingAfter(events, startTime); for (let i = startEvent; i < events.length; ++i) { const e = events[i]; const {endTime: eventEndTime, startTime: eventStartTime, duration: eventDuration} = SDK.TracingModel.timesForEventInMilliseconds(e); const eventPhase = SDK.TracingModel.phaseForEvent(e); if ((eventEndTime || eventStartTime) < startTime) { continue; } if (eventStartTime >= endTime) { break; } const canIgnoreAsyncEvent = ignoreAsyncEvents && TraceEngine.Types.TraceEvents.isAsyncPhase(eventPhase); if (canIgnoreAsyncEvent || TraceEngine.Types.TraceEvents.isFlowPhase(eventPhase)) { continue; } let last = stack[stack.length - 1]; let lastEventEndTime = last && SDK.TracingModel.timesForEventInMilliseconds(last).endTime; while (last && lastEventEndTime !== undefined && lastEventEndTime <= eventStartTime) { stack.pop(); onEndEvent(last); last = stack[stack.length - 1]; lastEventEndTime = last && SDK.TracingModel.timesForEventInMilliseconds(last).endTime; } if (filter && !filter(e)) { continue; } if (eventDuration) { 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); } } } private static topLevelEventEndingAfter(events: SDK.TracingModel.CompatibleTraceEvent[], time: number): number { let index = Platform.ArrayUtilities.upperBound( events, time, (time, event) => time - SDK.TracingModel.timesForEventInMilliseconds(event).startTime) - 1; while (index > 0 && !SDK.TracingModel.TracingModel.isTopLevelEvent(events[index])) { index--; } return Math.max(index, 0); } mainFrameID(): string { return this.mainFrame.frameId; } /** * Determines if an event is potentially a marker event. A marker event here * is a single moment in time that we want to highlight on the timeline, such as * the LCP point. This method does not filter out events: for example, it treats * every LCP Candidate event as a potential marker event. The logic to pick the * right candidate to use is implemeneted in the TimelineFlameChartDataProvider. **/ isMarkerEvent(event: SDK.TracingModel.CompatibleTraceEvent): boolean { switch (event.name) { case RecordType.TimeStamp: return true; case RecordType.MarkFirstPaint: case RecordType.MarkFCP: return Boolean(this.mainFrame) && event.args.frame === this.mainFrame.frameId && Boolean(event.args.data); case RecordType.MarkDOMContent: case RecordType.MarkLoad: case RecordType.MarkLCPCandidate: case RecordType.MarkLCPInvalidate: return Boolean(event.args['data']['isOutermostMainFrame'] ?? event.args['data']['isMainFrame']); default: return false; } } isInteractiveTimeEvent(event: SDK.TracingModel.Event): boolean { return event.name === RecordType.InteractiveTime; } isLayoutShiftEvent(event: SDK.TracingModel.Event): boolean { return event.name === RecordType.LayoutShift; } isParseHTMLEvent(event: SDK.TracingModel.Event): boolean { return event.name === RecordType.ParseHTML; } static isJsFrameEvent(event: SDK.TracingModel.CompatibleTraceEvent): boolean { return event.name === RecordType.JSFrame || event.name === RecordType.JSIdleFrame || event.name === RecordType.JSSystemFrame; } static globalEventId(event: SDK.TracingModel.Event, field: string): string { const data = event.args['data'] || event.args['beginData']; const id = data && data[field]; if (!id) { return ''; } return `${event.thread.process().id()}.${id}`; } static eventFrameId(event: SDK.TracingModel.Event): Protocol.Page.FrameId|null { const data = event.args['data'] || event.args['beginData']; return data && data['frame'] || null; } cpuProfiles(): SDK.CPUProfileDataModel.CPUProfileDataModel[] { return this.cpuProfilesInternal; } totalBlockingTime(): { time: number, estimated: boolean, } { if (this.totalBlockingTimeInternal === -1) { return {time: this.estimatedTotalBlockingTime, estimated: true}; } return {time: this.totalBlockingTimeInternal, estimated: false}; } targetByEvent(event: SDK.TracingModel.CompatibleTraceEvent): SDK.Target.Target|null { let thread; if (event instanceof SDK.TracingModel.Event) { thread = event.thread; } else { const process = this.tracingModelInternal?.getProcessById(event.pid); thread = process?.threadById(event.tid); } if (!thread) { return null; } // FIXME: Consider returning null for loaded traces. const workerId = this.workerIdByThread.get(thread); const primaryPageTarget = SDK.TargetManager.TargetManager.instance().primaryPageTarget(); return workerId ? SDK.TargetManager.TargetManager.instance().targetById(workerId) : primaryPageTarget; } navStartTimes(): Map<string, SDK.TracingModel.PayloadEvent> { if (!this.tracingModelInternal) { return new Map(); } return this.tracingModelInternal.navStartTimes(); } isFreshRecording(): boolean { return this.#isFreshRecording; } setEvents(tracingModel: SDK.TracingModel.TracingModel, isFreshRecording: boolean = false): void { this.#isFreshRecording = isFreshRecording; this.reset(); this.resetProcessingState(); this.tracingModelInternal = tracingModel; this.minimumRecordTimeInternal = tracingModel.minimumRecordTime(); this.maximumRecordTimeInternal = 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.isGenericTraceInternal = !metadataEvents; if (metadataEvents) { this.processMetadataAndThreads(tracingModel, metadataEvents); } else { this.processGenericTrace(tracingModel); } } this.inspectedTargetEventsInternal.sort(SDK.TracingModel.Event.compareStartTime); this.processAsyncBrowserEvents(tracingModel); this.resetProcessingState(); } private processGenericTrace(tracingModel: SDK.TracingModel.TracingModel): void { 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, thread, thread === browserMainThread, false, true, WorkletType.NotWorklet, null); } } } private processMetadataAndThreads(tracingModel: SDK.TracingModel.TracingModel, metadataEvents: MetadataEvents): void { 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: Platform.DevToolsPath.UrlString|null = 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; } const frameId = TimelineModelImpl.eventFrameId(e); return frameId ? Boolean(this.pageFrames.get(frameId)) : false; }); if (!workerMetaEvent) { continue; } const workerId = workerMetaEvent.args['data']['workerId']; if (workerId) { this.workerIdByThread.set(thread, workerId); } workerUrl = workerMetaEvent.args['data']['url'] || Platform.DevToolsPath.EmptyUrlString; } this.processThreadEvents( tracingModel, thread, thread === metaEvent.thread, Boolean(workerUrl), true, WorkletType.NotWorklet, workerUrl); } startTime = endTime; } } private processThreadsForBrowserFrames(tracingModel: SDK.TracingModel.TracingModel): void { const processDataByPid = new Map<number, { from: number, to: number, main: boolean, workletType: WorkletType, url: Platform.DevToolsPath.UrlString, }[]>(); for (const frame of this.pageFrames.values()) { for (let i = 0; i < frame.processes.length; i++) { const pid = frame.processes[i].processId; let data = processDataByPid.get(pid); if (!data) { data = []; processDataByPid.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, workletType: WorkletType.NotWorklet, }); } } for (const auctionWorklet of this.auctionWorklets.values()) { const pid = auctionWorklet.processId; let data = processDataByPid.get(pid); if (!data) { data = []; processDataByPid.set(pid, data); } data.push({ from: auctionWorklet.startTime, to: auctionWorklet.endTime, main: false, workletType: auctionWorklet.workletType, url: (auctionWorklet.host ? 'https://' + auctionWorklet.host as Platform.DevToolsPath.UrlString : Platform.DevToolsPath.EmptyUrlString), }); } const allMetadataEvents = tracingModel.devToolsMetadataEvents(); for (const process of tracingModel.sortedProcesses()) { const processData = processDataByPid.get(process.id()); if (!processData) { continue; } // Sort ascending by range starts, followed by range ends processData.sort((a, b) => a.from - b.from || a.to - b.to); let lastUrl: Platform.DevToolsPath.UrlString|null = null; let lastMainUrl: Platform.DevToolsPath.UrlString|null = null; let hasMain = false; let allWorklet = true; // false: not set, true: inconsistent. let workletUrl: Platform.DevToolsPath.UrlString|boolean = false; // NotWorklet used for not set. let workletType: WorkletType = WorkletType.NotWorklet; for (const item of processData) { if (item.main) { hasMain = true; } if (item.url) { if (item.main) { lastMainUrl = item.url; } lastUrl = item.url; } // Worklet identification if (item.workletType === WorkletType.NotWorklet) { allWorklet = false; } else { // Update combined workletUrl, checking for inconsistencies. if (workletUrl === false) { workletUrl = item.url; } else if (workletUrl !== item.url) { workletUrl = true; // Process used for different things. } if (workletType === WorkletType.NotWorklet) { workletType = item.workletType; } else if (workletType !== item.workletType) { workletType = WorkletType.UnknownWorklet; } } } for (const thread of process.sortedThreads()) { if (thread.name() === TimelineModelImpl.RendererMainThreadName) { this.processThreadEvents( tracingModel, thread, true /* isMainThread */, false /* isWorker */, hasMain, WorkletType.NotWorklet, 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; } const frameId = TimelineModelImpl.eventFrameId(e); return frameId ? Boolean(this.pageFrames.get(frameId)) : false; }); if (!workerMetaEvent) { continue; } this.workerIdByThread.set(thread, workerMetaEvent.args['data']['workerId'] || ''); this.processThreadEvents( tracingModel, thread, false /* isMainThread */, true /* isWorker */, false /* forMainFrame */, WorkletType.NotWorklet, workerMetaEvent.args['data']['url'] || Platform.DevToolsPath.EmptyUrlString); } else { let urlForOther: Platform.DevToolsPath.UrlString|null = null; let workletTypeForOther: WorkletType = WorkletType.NotWorklet; if (thread.name() === TimelineModelImpl.AuctionWorkletThreadName || thread.name().endsWith(TimelineModelImpl.UtilityMainThreadNameSuffix)) { if (typeof workletUrl !== 'boolean') { urlForOther = workletUrl; } workletTypeForOther = workletType; } else { // For processes that only do auction worklet things, skip other threads. if (allWorklet) { continue; } } this.processThreadEvents( tracingModel, thread, false /* isMainThread */, false /* isWorker */, false /* forMainFrame */, workletTypeForOther, urlForOther); } } } } private processMetadataEvents(tracingModel: SDK.TracingModel.TracingModel): MetadataEvents|null { 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']) || [] as PageFrame[]); frames.forEach((payload: PageFrame) => 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<any>(); function checkSessionId(event: SDK.TracingModel.Event): boolean { 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; } private processSyncBrowserEvents(tracingModel: SDK.TracingModel.TracingModel): void { const browserMain = SDK.TracingModel.TracingModel.browserMainThread(tracingModel); if (browserMain) { browserMain.events().forEach(this.processBrowserEvent, this); } } private processAsyncBrowserEvents(tracingModel: SDK.TracingModel.TracingModel): void { const browserMain = SDK.TracingModel.TracingModel.browserMainThread(tracingModel); if (browserMain) { this.processAsyncEvents(browserMain); } } private resetProcessingState(): void { 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.browserFrameTracking = false; this.persistentIds = false; this.legacyCurrentPage = null; } private extractCpuProfileDataModel(tracingModel: SDK.TracingModel.TracingModel, thread: SDK.TracingModel.Thread): SDK.CPUProfileDataModel.CPUProfileDataModel|null { const events = thread.events(); let cpuProfile; let target: (SDK.Target.Target|null)|null = null; // Check for legacy CpuProfile event format first. // 'CpuProfile' is currently used by https://webpack.js.org/plugins/profiling-plugin/ and our createFakeTraceFromCpuProfile let cpuProfileEvent = events.at(-1); if (cpuProfileEvent && cpuProfileEvent.name === RecordType.CpuProfile) { const eventData = cpuProfileEvent.args['data']; cpuProfile = (eventData && eventData['cpuProfile'] as Protocol.Profiler.Profile | null); target = this.targetByEvent(cpuProfileEvent); } if (!cpuProfile) { cpuProfileEvent = events.find(e => e.name === RecordType.Profile); if (!cpuProfileEvent) { return null; } target = this.targetByEvent(cpuProfileEvent); // Profile groups are created right after a trace is loaded (in // tracing model). // They are created using events with the "P" phase (samples), // which includes ProfileChunks with the samples themselves but // also "Profile" events with metadata of the profile. // A group is created for each unique profile in each unique // thread. const profileGroup = tracingModel.profileGroup(cpuProfileEvent); if (!profileGroup) { Common.Console.Console.instance().error('Invalid CPU profile format.'); return null; } cpuProfile = ({ startTime: cpuProfileEvent.startTime * 1000, endTime: 0, nodes: [], samples: [], timeDeltas: [], lines: [], } as any); 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) { const timeDeltas: number[] = cpuProfile.timeDeltas; cpuProfile.endTime = timeDeltas.reduce((x, y) => x + y, cpuProfile.startTime); } } try { const profile = (cpuProfile as Protocol.Profiler.Profile); const jsProfileModel = new SDK.CPUProfileDataModel.CPUProfileDataModel(profile, target); this.cpuProfilesInternal.push(jsProfileModel); return jsProfileModel; } catch (e) { Common.Console.Console.instance().error('Failed to parse CPU profile.'); } return null; } private injectJSFrameEvents(tracingModel: SDK.TracingModel.TracingModel, thread: SDK.TracingModel.Thread): SDK.TracingModel.Event[] { const jsProfileModel = this.extractCpuProfileDataModel(tracingModel, thread); let events = thread.events(); const jsSamples = jsProfileModel ? TimelineJSProfileProcessor.generateConstructedEventsFromCpuProfileDataModel(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 || e.name === RecordType.JSSystemSample || e.name === RecordType.JSIdleSample)) { 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; } private static nameAuctionWorklet(workletType: WorkletType, url: Platform.DevToolsPath.UrlString|null): string { switch (workletType) { case WorkletType.BidderWorklet: return url ? i18nString(UIStrings.bidderWorkletS, {PH1: url}) : i18nString(UIStrings.bidderWorklet); case WorkletType.SellerWorklet: return url ? i18nString(UIStrings.sellerWorkletS, {PH1: url}) : i18nString(UIStrings.sellerWorklet); default: return url ? i18nString(UIStrings.unknownWorkletS, {PH1: url}) : i18nString(UIStrings.unknownWorklet); } } private processThreadEvents( tracingModel: SDK.TracingModel.TracingModel, thread: SDK.TracingModel.Thread, isMainThread: boolean, isWorker: boolean, forMainFrame: boolean, workletType: WorkletType, url: Platform.DevToolsPath.UrlString|null): void { 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 || Platform.DevToolsPath.EmptyUrlString; track.forMainFrame = forMainFrame; } else if (isWorker) { track.type = TrackType.Worker; track.url = url || Platform.DevToolsPath.EmptyUrlString; track.name = track.url ? i18nString(UIStrings.workerS, {PH1: track.url}) : i18nString(UIStrings.dedicatedWorker); } else if (thread.name().startsWith('CompositorTileWorker')) { track.type = TrackType.Raster; } else if (thread.name() === TimelineModelImpl.AuctionWorkletThreadName) { track.url = url || Platform.DevToolsPath.EmptyUrlString; track.name = TimelineModelImpl.nameAuctionWorklet(workletType, url); } else if ( workletType !== WorkletType.NotWorklet && thread.name().endsWith(TimelineModelImpl.UtilityMainThreadNameSuffix)) { track.url = url || Platform.DevToolsPath.EmptyUrlString; track.name = url ? i18nString(UIStrings.workletServiceS, {PH1: url}) : i18nString(UIStrings.workletService); } this.tracksInternal.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 (let i = 0; i < events.length; i++) { const event = events[i]; // There may be several TTI events, only take the first one. if (this.isInteractiveTimeEvent(event) && this.totalBlockingTimeInternal === -1) { this.totalBlockingTimeInternal = 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: SDK.TracingModel.Event = 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 (!TraceEngine.Types.TraceEvents.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.timeMarkerEventsInternal.push(event); } track.events.push(event); this.inspectedTargetEventsInternal.push(event); } this.processAsyncEvents(thread); } private fixNegativeDuration(event: SDK.TracingModel.Event, child: SDK.TracingModel.Event): void { 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; } private processAsyncEvents(thread: SDK.TracingModel.Thread): void { const asyncEvents = thread.asyncEvents(); const groups = new Map<TrackType, SDK.TracingModel.AsyncEvent[]>(); function group(type: TrackType): SDK.TracingModel.AsyncEvent[] { if (!groups.has(type)) { groups.set(type, []); } return groups.get(type) as SDK.TracingModel.AsyncEvent[]; } for (let i = 0; i < asyncEvents.length; ++i) { const asyncEvent = asyncEvents[i]; if (asyncEvent.name === RecordType.Animation) { group(TrackType.Animation).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); } } private processEvent(event: SDK.TracingModel.Event): boolean { 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 = EventOnTimelineData.forEvent(e); timelineData.warning = e.name === RecordType.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 = EventOnTimelineData.forEvent(event); if (eventData['stackTrace']) { timelineData.stackTrace = eventData['stackTrace'].map((callFrameOrProfileNode: Protocol.Runtime.CallFrame) => { // `callFrameOrProfileNode` can also be a `SDK.ProfileTreeModel.ProfileNode` for JSSample; that class // has accessors to mimic a `CallFrame`, but apparently we don't adjust stack traces in that case. Whether // we should is unclear. if (event.name !== RecordType.JSSample && event.name !== RecordType.JSSystemSample && event.name !== RecordType.JSIdleSample) { // We need to copy the data so we can safely modify it below. const frame = {...callFrameOrProfileNode}; // TraceEvents come with 1-based line & column numbers. The frontend code // requires 0-based ones. Adjust the values. --frame.lineNumber; --frame.columnNumber; return frame; } return callFrameOrProfileNode; }); } let pageFrameId = TimelineModelImpl.eventFrameId(event); const last = eventStack[eventStack.length - 1]; if (!pageFrameId && last) { pageFrameId = EventOnTimelineData.forEvent(last).frameId; } timelineData.frameId = pageFrameId || (this.mainFrame && this.mainFrame.frameId) || ''; this.asyncEventTracker.processEvent(event); switch (event.name) { case RecordType.ResourceSendRequest: case RecordType.WebSocketCreate: { timelineData.setInitiator(eventStack[eventStack.length - 1] || null); timelineData.url = eventData['url']; break; } case RecordType.ScheduleStyleRecalculation: { this.lastScheduleStyleRecalculation[eventData['frame']] = event; break; } case RecordType.UpdateLayoutTree: case RecordType.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 RecordType.ScheduleStyleInvalidationTracking: case RecordType.StyleRecalcInvalidationTracking: case RecordType.StyleInvalidatorInvalidationTracking: case RecordType.LayoutInvalidationTracking: { this.invalidationTracker.addInvalidation(new InvalidationTrackingEvent(event, timelineData)); break; } case RecordType.InvalidateLayout: { // Consider style recalculation as a reason for layout invalidation, // but only if we had no earlier layout invalidation records. let layoutInitator: (SDK.TracingModel.Event|null)|SDK.TracingModel.Event = event; const frameId = eventData['frame']; if (!this.layoutInvalidate[frameId] && this.lastRecalculateStylesEvent && this.lastRecalculateStylesEvent.endTime !== undefined && this.lastRecalculateStylesEvent.endTime > event.startTime) { layoutInitator = EventOnTimelineData.forEvent(this.lastRecalculateStylesEvent).initiator(); } this.layoutInvalidate[frameId] = layoutInitator; break; } case RecordType.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']) { if (event.args['endData']['layoutRoots']) { for (let i = 0; i < event.args['endData']['layoutRoots'].length; ++i) { timelineData.backendNodeIds.push(event.args['endData']['layoutRoots'][i]['nodeId']); } } else { timelineData.backendNodeIds.push(event.args['endData']['rootNode']); } } this.layoutInvalidate[frameId] = null; if (this.currentScriptEvent) { this.currentTaskLayoutAndRecalcEvents.push(event); } break; } case RecordType.Task: { if (event.duration !== undefined && event.duration > TimelineModelImpl.Thresholds.LongTask) { timelineData.warning = TimelineModelImpl.WarningType.LongTask; } break; } case RecordType.EventDispatch: { if (event.duration !== undefined && event.duration > TimelineModelImpl.Thresholds.RecurringHandler) { timelineData.warning = TimelineModelImpl.WarningType.LongHandler; } break; } case RecordType.TimerFire: case RecordType.FireAnimationFrame: { if (event.duration !== undefined && event.duration > TimelineModelImpl.Thresholds.RecurringHandler) { timelineData.warning = TimelineModelImpl.WarningType.LongRecurringHandler; } break; } // @ts-ignore fallthrough intended. case RecordType.FunctionCall: { // Compatibility with old format. if (typeof eventData['scriptName'] === 'string') { eventData['url'] = eventData['scriptName']; } if (typeof eventData['scriptLine'] === 'number') { eventData['lineNumber'] = eventData['scriptLine']; } } case RecordType.EvaluateScript: case RecordType.CompileScript: // @ts-ignore fallthrough intended. case RecordType.CacheScript: { if (typeof eventData['lineNumber'] === 'number') { --eventData['lineNumber']; } if (typeof eventData['columnNumber'] === 'number') { --eventData['columnNumber']; } } case RecordType.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 RecordType.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 = frameId ? this.pageFrames.get(frameId) : null; if (!pageFrame || pageFrame.parent) { return false; } this.mainFrameLayerTreeId = eventData['layerTreeId']; break; } case RecordType.Paint: { this.invalidationTracker.didPaint = true; // With CompositeAfterPaint enabled, paint events are no longer // associated with a Node, and nodeId will not be present. if ('nodeId' in eventData) { timelineData.backendNodeIds.push(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 RecordType.DisplayItemListSnapshot: case RecordType.PictureSnapshot: { // If we get a snapshot, we try to find the last Paint event for the // current layer, and store the snapshot as the relevant picture for // that event, thus creating a relationship between the snapshot and // the last Paint event for the current timestamp. const layerUpdateEvent = this.findAncestorEvent(RecordType.UpdateLayer); if (!layerUpdateEvent || layerUpdateEvent.args['layerTreeId'] !== this.mainFrameLayerTreeId) { break; } const paintEvent = this.lastPaintForLayer[layerUpdateEvent.args['layerId']]; if (paintEvent) { EventOnTimelineData.forEvent(paintEvent).picture = (event as SDK.TracingModel.ObjectSnapshot); } break; } case RecordType.ScrollLayer: { timelineData.backendNodeIds.push(eventData['nodeId']); break; } case RecordType.PaintImage: { timelineData.backendNodeIds.push(eventData['nodeId']); timelineData.url = eventData['url']; break; } case RecordType.DecodeImage: case RecordType.ResizeImage: { let paintImageEvent = this.findAncestorEvent(RecordType.PaintImage); if (!paintImageEvent) { const decodeLazyPixelRefEvent = this.findAncestorEvent(RecordType.DecodeLazyPixelRef); paintImageEvent = decodeLazyPixelRefEvent && this.paintImageEventByPixelRefId[decodeLazyPixelRefEvent.args['LazyPixelRef']]; } if (!paintImageEvent) { break; } const paintImageData = EventOnTimelineData.forEvent(paintImageEvent); timelineData.backendNodeIds.push(paintImageData.backendNodeIds[0]); timelineData.url = paintImageData.url; break; } case RecordType.DrawLazyPixelRef: { const paintImageEvent = this.findAncestorEvent(RecordType.PaintImage); if (!paintImageEvent) { break; } this.paintImageEventByPixelRefId[event.args['LazyPixelRef']] = paintImageEvent; const paintImageData = EventOnTimelineData.forEvent(paintImageEvent); timelineData.backendNodeIds.push(paintImageData.backendNodeIds[0]); timelineData.url = paintImageData.url; break; } case RecordType.FrameStartedLoading: { if (timelineData.frameId !== event.args['frame']) { return false; } break; } case RecordType.MarkLCPCandidate: { timelineData.backendNodeIds.push(eventData['nodeId']); break; } case RecordType.MarkDOMContent: case RecordType.MarkLoad: { const frameId = TimelineModelImpl.eventFrameId(event); if (!frameId || !this.pageFrames.has(frameId)) { return false; } break; } case RecordType.CommitLoad: { if (this.browserFrameTracking) { break; } const frameId = TimelineModelImpl.eventFrameId(event); const isOutermostMainFrame = Boolean(eventData['isOutermostMainFrame'] ?? eventData['isMainFrame']); const pageFrame = frameId ? this.pageFrames.get(frameId) : null;