UNPKG

chrome-devtools-frontend

Version:
1,249 lines (1,140 loc) 95.5 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 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 Text shown when rendering the User Interactions track in the Performance panel */ userInteractions: 'User Interactions', /** *@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; constructor() { this.minimumRecordTimeInternal = 0; this.maximumRecordTimeInternal = 0; this.totalBlockingTimeInternal = 0; this.estimatedTotalBlockingTime = 0; this.reset(); this.resetProcessingState(); this.currentTaskLayoutAndRecalcEvents = []; this.tracingModelInternal = null; } static forEachEvent( events: SDK.TracingModel.Event[], onStartEvent: (arg0: SDK.TracingModel.Event) => void, onEndEvent: (arg0: SDK.TracingModel.Event) => void, onInstantEvent?: ((arg0: SDK.TracingModel.Event, arg1: SDK.TracingModel.Event|null) => void), startTime?: number, endTime?: number, filter?: ((arg0: SDK.TracingModel.Event) => boolean)): void { startTime = startTime || 0; endTime = endTime || Infinity; const stack: SDK.TracingModel.Event[] = []; 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: SDK.TracingModel.Event = 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); } } } private static topLevelEventEndingAfter(events: SDK.TracingModel.Event[], time: number): number { 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); } isMarkerEvent(event: SDK.TracingModel.Event): 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; } isUserTimingEvent(event: SDK.TracingModel.Event): boolean { return event.categoriesString === TimelineModelImpl.Category.UserTiming; } isEventTimingInteractionEvent(event: SDK.TracingModel.Event): boolean { if (event.name !== RecordType.EventTiming) { return false; } type InteractionEventData = { duration?: number, interactionId: number, }; const data = event.args.data as InteractionEventData; // Filter out: // 1. events without a duration, or a duration of 0 // 2. events without an interactionId, or with an interactionId of 0, // which indicates that it's not a "top level" interaction event and // we can therefore ignore it. This can happen with "mousedown" for // example; an interaction ID is assigned to the "pointerdown" event // as it's the "first" event to be triggered when the user clicks, // but the browser doesn't attempt to assign IDs to all subsequent // events, as that's a hard heuristic to get right. const duration = data.duration || 0; const interactionId = data.interactionId || 0; return (duration > 0 && interactionId > 0); } isParseHTMLEvent(event: SDK.TracingModel.Event): boolean { return event.name === RecordType.ParseHTML; } isLCPCandidateEvent(event: SDK.TracingModel.Event): boolean { return event.name === RecordType.MarkLCPCandidate && Boolean(event.args['data']['isOutermostMainFrame'] ?? event.args['data']['isMainFrame']); } isLCPInvalidateEvent(event: SDK.TracingModel.Event): boolean { return event.name === RecordType.MarkLCPInvalidate && Boolean(event.args['data']['isOutermostMainFrame'] ?? event.args['data']['isMainFrame']); } isFCPEvent(event: SDK.TracingModel.Event): boolean { return event.name === RecordType.MarkFCP && Boolean(this.mainFrame) && event.args['frame'] === this.mainFrame.frameId; } isLongRunningTask(event: SDK.TracingModel.Event): boolean { return event.name === RecordType.Task && TimelineData.forEvent(event).warning === TimelineModelImpl.WarningType.LongTask; } isNavigationStartEvent(event: SDK.TracingModel.Event): boolean { return event.name === RecordType.NavigationStart; } isMainFrameNavigationStartEvent(event: SDK.TracingModel.Event): boolean { return this.isNavigationStartEvent(event) && (event.args['data']['isOutermostMainFrame'] ?? event.args['data']['isLoadingMainFrame']) && event.args['data']['documentLoaderURL']; } 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.Event): SDK.Target.Target|null { // FIXME: Consider returning null for loaded traces. const workerId = this.workerIdByThread.get(event.thread); const mainTarget = SDK.TargetManager.TargetManager.instance().mainTarget(); return workerId ? SDK.TargetManager.TargetManager.instance().targetById(workerId) : mainTarget; } navStartTimes(): Map<string, SDK.TracingModel.Event> { if (!this.tracingModelInternal) { return new Map(); } return this.tracingModelInternal.navStartTimes(); } setEvents(tracingModel: SDK.TracingModel.TracingModel): void { 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.buildGPUEvents(tracingModel); this.buildLoadingEvents(tracingModel, layoutShiftEvents); this.collectInteractionEvents(tracingModel); this.resetProcessingState(); } private collectInteractionEvents(tracingModel: SDK.TracingModel.TracingModel): void { const interactionEvents: SDK.TracingModel.AsyncEvent[] = []; for (const process of tracingModel.sortedProcesses()) { // Interactions will only appear on the Renderer processes. if (process.name() !== 'Renderer') { continue; } // And also only on CrRendererMain threads. const rendererThread = process.threadByName('CrRendererMain'); if (!rendererThread) { continue; } // EventTiming events are async, so we only have to check asyncEvents, // and not worry about sync events. for (const event of rendererThread.asyncEvents()) { if (!this.isEventTimingInteractionEvent(event)) { continue; } interactionEvents.push(event); } } if (interactionEvents.length === 0) { // No events found, so bail early and don't bother creating the track // because it will be empty. return; } const track = this.ensureNamedTrack(TrackType.UserInteractions); track.name = UIStrings.userInteractions; track.forMainFrame = true; track.asyncEvents = interactionEvents; } 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, [{from: 0, to: Infinity}], 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, [{from: startTime, to: endTime}], thread, thread === metaEvent.thread, Boolean(workerUrl), true, WorkletType.NotWorklet, workerUrl); } startTime = endTime; } } private processThreadsForBrowserFrames(tracingModel: SDK.TracingModel.TracingModel): void { const processData = 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 = 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, workletType: WorkletType.NotWorklet, }); } } for (const auctionWorklet of this.auctionWorklets.values()) { const pid = auctionWorklet.processId; let data = processData.get(pid); if (!data) { data = []; processData.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 data = processData.get(process.id()); if (!data) { continue; } data.sort((a, b) => a.from - b.from || a.to - b.to); const ranges = []; 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 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.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; } } 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, 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, ranges, 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() === TimelineModelImpl.UtilityMainThreadName) { 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, ranges, 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, [{from: 0, to: Infinity}]); } } private buildGPUEvents(tracingModel: SDK.TracingModel.TracingModel): void { const thread = tracingModel.getThreadByName('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); } private buildLoadingEvents(tracingModel: SDK.TracingModel.TracingModel, events: SDK.TracingModel.Event[]): void { const thread = tracingModel.getThreadByName('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); if (eventData['impacted_nodes']) { for (let i = 0; i < eventData['impacted_nodes'].length; ++i) { timelineData.backendNodeIds.push(eventData['impacted_nodes'][i]['node_id']); } } } } } 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 extractCpuProfile(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. let cpuProfileEvent: (SDK.TracingModel.Event|undefined)|SDK.TracingModel.Event = events[events.length - 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); 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.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; } 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, ranges: { from: number, to: number, }[], 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() === TimelineModelImpl.UtilityMainThreadName) { 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 (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.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 (!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.timeMarkerEventsInternal.push(event); } track.events.push(event); this.inspectedTargetEventsInternal.push(event); } } this.processAsyncEvents(thread, ranges); } 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, ranges: { from: number, to: number, }[]): 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 (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; } } } 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 = TimelineData.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 = TimelineData.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) { // 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 = 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 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 = TimelineData.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.DisplayItemListSna