UNPKG

chrome-devtools-frontend

Version:
561 lines (493 loc) • 20.8 kB
// Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Platform from '../../../core/platform/platform.js'; import * as Helpers from '../helpers/helpers.js'; import * as Types from '../types/types.js'; import {type AuctionWorkletsData, data as auctionWorkletsData} from './AuctionWorkletsHandler.js'; import {data as layerTreeHandlerData, type LayerTreeData} from './LayerTreeHandler.js'; import {data as metaHandlerData, type MetaHandlerData} from './MetaHandler.js'; import {data as rendererHandlerData, type RendererHandlerData} from './RendererHandler.js'; import * as Threads from './Threads.js'; import type {HandlerName} from './types.js'; /** * IMPORTANT: this handler is slightly different to the rest. This is because * it is an adaptation of the TimelineFrameModel that has been used in DevTools * for many years. Rather than re-implement all the logic from scratch, instead * this handler gathers up the events and instantitates the class in the * finalize() method. Once the class has parsed all events, it is used to then * return the array of frames. * * In time we expect to migrate this code to a more "typical" handler. */ const allEvents: Types.Events.Event[] = []; let model: TimelineFrameModel|null = null; export function reset(): void { allEvents.length = 0; } export function handleEvent(event: Types.Events.Event): void { allEvents.push(event); } export async function finalize(): Promise<void> { // Snapshot events can be emitted out of order, so we need to sort before // building the frames model. Helpers.Trace.sortTraceEventsInPlace(allEvents); const modelForTrace = new TimelineFrameModel( allEvents, rendererHandlerData(), auctionWorkletsData(), metaHandlerData(), layerTreeHandlerData(), ); model = modelForTrace; } export interface FramesData { frames: readonly Types.Events.LegacyTimelineFrame[]; framesById: Readonly<Record<number, Types.Events.LegacyTimelineFrame|undefined>>; } export function data(): FramesData { return { frames: model ? Array.from(model.frames()) : [], framesById: model ? {...model.framesById()} : {}, }; } export function deps(): HandlerName[] { return ['Meta', 'Renderer', 'AuctionWorklets', 'LayerTree']; } type FrameEvent = Types.Events.BeginFrame|Types.Events.DroppedFrame|Types.Events.RequestMainThreadFrame| Types.Events.BeginMainThreadFrame|Types.Events.Commit|Types.Events.CompositeLayers| Types.Events.ActivateLayerTree|Types.Events.NeedsBeginFrameChanged|Types.Events.DrawFrame; function isFrameEvent(event: Types.Events.Event): event is FrameEvent { return ( Types.Events.isSetLayerId(event) || Types.Events.isBeginFrame(event) || Types.Events.isDroppedFrame(event) || Types.Events.isRequestMainThreadFrame(event) || Types.Events.isBeginMainThreadFrame(event) || Types.Events.isNeedsBeginFrameChanged(event) || // Note that "Commit" is the replacement for "CompositeLayers" so in a trace // we wouldn't expect to see a combination of these. All "new" trace // recordings use "Commit", but we can easily support "CompositeLayers" too // to not break older traces being imported. Types.Events.isCommit(event) || Types.Events.isCompositeLayers(event) || Types.Events.isActivateLayerTree(event) || Types.Events.isDrawFrame(event)); } function entryIsTopLevel(entry: Types.Events.Event): boolean { const devtoolsTimelineCategory = 'disabled-by-default-devtools.timeline'; return entry.name === Types.Events.Name.RUN_TASK && entry.cat.includes(devtoolsTimelineCategory); } export class TimelineFrameModel { #frames: TimelineFrame[] = []; #frameById: Record<number, TimelineFrame> = {}; #beginFrameQueue: TimelineFrameBeginFrameQueue = new TimelineFrameBeginFrameQueue(); #lastFrame: TimelineFrame|null = null; #mainFrameCommitted = false; #mainFrameRequested = false; #lastLayerTree: Types.Events.LegacyFrameLayerTreeData|null = null; #framePendingActivation: PendingFrame|null = null; #framePendingCommit: PendingFrame|null = null; #lastBeginFrame: number|null = null; #lastNeedsBeginFrame: number|null = null; #lastTaskBeginTime: Types.Timing.Micro|null = null; #layerTreeId: number|null = null; #activeProcessId: Types.Events.ProcessID|null = null; #activeThreadId: Types.Events.ThreadID|null = null; #layerTreeData: LayerTreeData; constructor( allEvents: readonly Types.Events.Event[], rendererData: RendererHandlerData, auctionWorkletsData: AuctionWorkletsData, metaData: MetaHandlerData, layerTreeData: LayerTreeData) { // We only care about getting threads from the Renderer, not Samples, // because Frames don't exist in a CPU Profile (which won't have Renderer // threads.) const mainThreads = Threads.threadsInRenderer(rendererData, auctionWorkletsData).filter(thread => { return thread.type === Threads.ThreadType.MAIN_THREAD && thread.processIsOnMainFrame; }); const threadData = mainThreads.map(thread => { return { tid: thread.tid, pid: thread.pid, startTime: thread.entries[0].ts, }; }); this.#layerTreeData = layerTreeData; this.#addTraceEvents(allEvents, threadData, metaData.mainFrameId); } framesById(): Readonly<Record<number, TimelineFrame|undefined>> { return this.#frameById; } frames(): TimelineFrame[] { return this.#frames; } #handleBeginFrame(startTime: Types.Timing.Micro, seqId: number): void { if (!this.#lastFrame) { this.#startFrame(startTime, seqId); } this.#lastBeginFrame = startTime; this.#beginFrameQueue.addFrameIfNotExists(seqId, startTime, false, false); } #handleDroppedFrame(startTime: Types.Timing.Micro, seqId: number, isPartial: boolean): void { if (!this.#lastFrame) { this.#startFrame(startTime, seqId); } // This line handles the case where no BeginFrame event is issued for // the dropped frame. In this situation, add a BeginFrame to the queue // as if it actually occurred. this.#beginFrameQueue.addFrameIfNotExists(seqId, startTime, true, isPartial); this.#beginFrameQueue.setDropped(seqId, true); this.#beginFrameQueue.setPartial(seqId, isPartial); } #handleDrawFrame(startTime: Types.Timing.Micro, seqId: number): void { if (!this.#lastFrame) { this.#startFrame(startTime, seqId); return; } // - if it wasn't drawn, it didn't happen! // - only show frames that either did not wait for the main thread frame or had one committed. if (this.#mainFrameCommitted || !this.#mainFrameRequested) { if (this.#lastNeedsBeginFrame) { const idleTimeEnd = this.#framePendingActivation ? this.#framePendingActivation.triggerTime : (this.#lastBeginFrame || this.#lastNeedsBeginFrame); if (idleTimeEnd > this.#lastFrame.startTime) { this.#lastFrame.idle = true; this.#lastBeginFrame = null; } this.#lastNeedsBeginFrame = null; } const framesToVisualize = this.#beginFrameQueue.processPendingBeginFramesOnDrawFrame(seqId); // Visualize the current frame and all pending frames before it. for (const frame of framesToVisualize) { const isLastFrameIdle = this.#lastFrame.idle; // If |frame| is the first frame after an idle period, the CPU time // will be logged ("committed") under |frame| if applicable. this.#startFrame(frame.startTime, seqId); if (isLastFrameIdle && this.#framePendingActivation) { this.#commitPendingFrame(); } if (frame.isDropped) { this.#lastFrame.dropped = true; } if (frame.isPartial) { this.#lastFrame.isPartial = true; } } } this.#mainFrameCommitted = false; } #handleActivateLayerTree(): void { if (!this.#lastFrame) { return; } if (this.#framePendingActivation && !this.#lastNeedsBeginFrame) { this.#commitPendingFrame(); } } #handleRequestMainThreadFrame(): void { if (!this.#lastFrame) { return; } this.#mainFrameRequested = true; } #handleCommit(): void { if (!this.#framePendingCommit) { return; } this.#framePendingActivation = this.#framePendingCommit; this.#framePendingCommit = null; this.#mainFrameRequested = false; this.#mainFrameCommitted = true; } #handleLayerTreeSnapshot(layerTree: Types.Events.LegacyFrameLayerTreeData): void { this.#lastLayerTree = layerTree; } #handleNeedFrameChanged(startTime: Types.Timing.Micro, needsBeginFrame: boolean): void { if (needsBeginFrame) { this.#lastNeedsBeginFrame = startTime; } } #startFrame(startTime: Types.Timing.Micro, seqId: number): void { if (this.#lastFrame) { this.#flushFrame(this.#lastFrame, startTime); } this.#lastFrame = new TimelineFrame(seqId, startTime, Types.Timing.Micro(startTime - metaHandlerData().traceBounds.min)); } #flushFrame(frame: TimelineFrame, endTime: Types.Timing.Micro): void { frame.setLayerTree(this.#lastLayerTree); frame.setEndTime(endTime); if (this.#lastLayerTree) { this.#lastLayerTree.paints = frame.paints; } const lastFrame = this.#frames[this.#frames.length - 1]; if (this.#frames.length && lastFrame && (frame.startTime !== lastFrame.endTime || frame.startTime > frame.endTime)) { console.assert( false, `Inconsistent frame time for frame ${this.#frames.length} (${frame.startTime} - ${frame.endTime})`); } const newFramesLength = this.#frames.push(frame); frame.setIndex(newFramesLength - 1); if (typeof frame.mainFrameId === 'number') { this.#frameById[frame.mainFrameId] = frame; } } #commitPendingFrame(): void { if (!this.#framePendingActivation || !this.#lastFrame) { return; } this.#lastFrame.paints = this.#framePendingActivation.paints; this.#lastFrame.mainFrameId = this.#framePendingActivation.mainFrameId; this.#framePendingActivation = null; } #addTraceEvents( events: readonly Types.Events.Event[], threadData: Array<{ pid: Types.Events.ProcessID, tid: Types.Events.ThreadID, startTime: Types.Timing.Micro, }>, mainFrameId: string): void { let j = 0; this.#activeThreadId = threadData.length && threadData[0].tid || null; this.#activeProcessId = threadData.length && threadData[0].pid || null; for (let i = 0; i < events.length; ++i) { while (j + 1 < threadData.length && threadData[j + 1].startTime <= events[i].ts) { this.#activeThreadId = threadData[++j].tid; this.#activeProcessId = threadData[j].pid; } this.#addTraceEvent(events[i], mainFrameId); } this.#activeThreadId = null; this.#activeProcessId = null; } #addTraceEvent(event: Types.Events.Event, mainFrameId: string): void { if (Types.Events.isSetLayerId(event) && event.args.data.frame === mainFrameId) { this.#layerTreeId = event.args.data.layerTreeId; } else if (Types.Events.isLayerTreeHostImplSnapshot(event) && Number(event.id) === this.#layerTreeId) { this.#handleLayerTreeSnapshot({ entry: event, paints: [], }); } else { if (isFrameEvent(event)) { this.#processCompositorEvents(event); } // Make sure we only use events from the main thread: we check the PID as // well in case two processes have a thread with the same TID. if (event.tid === this.#activeThreadId && event.pid === this.#activeProcessId) { this.#addMainThreadTraceEvent(event); } } } #processCompositorEvents(entry: FrameEvent): void { if (entry.args['layerTreeId'] !== this.#layerTreeId) { return; } if (Types.Events.isBeginFrame(entry)) { this.#handleBeginFrame(entry.ts, entry.args['frameSeqId']); } else if (Types.Events.isDrawFrame(entry)) { this.#handleDrawFrame(entry.ts, entry.args['frameSeqId']); } else if (Types.Events.isActivateLayerTree(entry)) { this.#handleActivateLayerTree(); } else if (Types.Events.isRequestMainThreadFrame(entry)) { this.#handleRequestMainThreadFrame(); } else if (Types.Events.isNeedsBeginFrameChanged(entry)) { // needsBeginFrame property will either be 0 or 1, which represents // true/false in this case, hence the Boolean() wrapper. this.#handleNeedFrameChanged(entry.ts, entry.args['data'] && Boolean(entry.args['data']['needsBeginFrame'])); } else if (Types.Events.isDroppedFrame(entry)) { this.#handleDroppedFrame(entry.ts, entry.args['frameSeqId'], Boolean(entry.args['hasPartialUpdate'])); } } #addMainThreadTraceEvent(entry: Types.Events.Event): void { if (entryIsTopLevel(entry)) { this.#lastTaskBeginTime = entry.ts; } if (!this.#framePendingCommit && MAIN_FRAME_MARKERS.has(entry.name as Types.Events.Name)) { this.#framePendingCommit = new PendingFrame(this.#lastTaskBeginTime || entry.ts); } if (!this.#framePendingCommit) { return; } if (Types.Events.isBeginMainThreadFrame(entry) && entry.args.data.frameId) { this.#framePendingCommit.mainFrameId = entry.args.data.frameId; } if (Types.Events.isPaint(entry)) { const snapshot = this.#layerTreeData.paintsToSnapshots.get(entry); if (snapshot) { this.#framePendingCommit.paints.push(new LayerPaintEvent(entry, snapshot)); } } // Commit will be replacing CompositeLayers but CompositeLayers is kept // around for backwards compatibility. if ((Types.Events.isCompositeLayers(entry) || Types.Events.isCommit(entry)) && entry.args['layerTreeId'] === this.#layerTreeId) { this.#handleCommit(); } } } const MAIN_FRAME_MARKERS = new Set<Types.Events.Name>([ Types.Events.Name.SCHEDULE_STYLE_RECALCULATION, Types.Events.Name.INVALIDATE_LAYOUT, Types.Events.Name.BEGIN_MAIN_THREAD_FRAME, Types.Events.Name.SCROLL_LAYER, ]); /** * Legacy class that represents TimelineFrames that was ported from the old SDK. * This class is purposefully not exported as it breaks the abstraction that * every event shown on the timeline is a trace event. Instead, we use the Type * LegacyTimelineFrame to represent frames in the codebase. These do implement * the right interface to be treated just like they were a trace event. */ class TimelineFrame implements Types.Events.LegacyTimelineFrame { // These fields exist to satisfy the base Event type which all // "trace events" must implement. They aren't used, but doing this means we // can pass `TimelineFrame` instances into places that expect // Types.Events.Event. cat = 'devtools.legacy_frame'; name = 'frame'; ph = Types.Events.Phase.COMPLETE; ts: Types.Timing.Micro; pid = Types.Events.ProcessID(-1); tid = Types.Events.ThreadID(-1); index = -1; startTime: Types.Timing.Micro; startTimeOffset: Types.Timing.Micro; endTime: Types.Timing.Micro; duration: Types.Timing.Micro; idle: boolean; dropped: boolean; isPartial: boolean; layerTree: Types.Events.LegacyFrameLayerTreeData|null; paints: LayerPaintEvent[]; mainFrameId: number|undefined; readonly seqId: number; constructor(seqId: number, startTime: Types.Timing.Micro, startTimeOffset: Types.Timing.Micro) { this.seqId = seqId; this.startTime = startTime; this.ts = startTime; this.startTimeOffset = startTimeOffset; this.endTime = this.startTime; this.duration = Types.Timing.Micro(0); this.idle = false; this.dropped = false; this.isPartial = false; this.layerTree = null; this.paints = []; this.mainFrameId = undefined; } setIndex(i: number): void { this.index = i; } setEndTime(endTime: Types.Timing.Micro): void { this.endTime = endTime; this.duration = Types.Timing.Micro(this.endTime - this.startTime); } setLayerTree(layerTree: Types.Events.LegacyFrameLayerTreeData|null): void { this.layerTree = layerTree; } /** * Fake the `dur` field to meet the expected value given that we pretend * these TimelineFrame classes are trace events across the codebase. */ get dur(): Types.Timing.Micro { return this.duration; } } export class LayerPaintEvent implements Types.Events.LegacyLayerPaintEvent { readonly #event: Types.Events.Paint; #snapshot: Types.Events.DisplayItemListSnapshot; constructor(event: Types.Events.Paint, snapshot: Types.Events.DisplayItemListSnapshot) { this.#event = event; this.#snapshot = snapshot; } layerId(): number { return this.#event.args.data.layerId; } event(): Types.Events.Paint { return this.#event; } picture(): Types.Events.LegacyLayerPaintEventPicture|null { const rect = this.#snapshot.args.snapshot.params?.layer_rect; const pictureData = this.#snapshot.args.snapshot.skp64; return rect && pictureData ? {rect, serializedPicture: pictureData} : null; } } export class PendingFrame { paints: LayerPaintEvent[]; mainFrameId: number|undefined; triggerTime: number; constructor(triggerTime: number) { this.paints = []; this.mainFrameId = undefined; this.triggerTime = triggerTime; } } // The parameters of an impl-side BeginFrame. class BeginFrameInfo { seqId: number; startTime: Types.Timing.Micro; isDropped: boolean; isPartial: boolean; constructor(seqId: number, startTime: Types.Timing.Micro, isDropped: boolean, isPartial: boolean) { this.seqId = seqId; this.startTime = startTime; this.isDropped = isDropped; this.isPartial = isPartial; } } // A queue of BeginFrames pending visualization. // BeginFrames are added into this queue as they occur; later when their // corresponding DrawFrames occur (or lack thereof), the BeginFrames are removed // from the queue and their timestamps are used for visualization. export class TimelineFrameBeginFrameQueue { private queueFrames: number[] = []; // Maps frameSeqId to BeginFrameInfo. private mapFrames: Record<number, BeginFrameInfo> = {}; // Add a BeginFrame to the queue, if it does not already exit. addFrameIfNotExists(seqId: number, startTime: Types.Timing.Micro, isDropped: boolean, isPartial: boolean): void { if (!(seqId in this.mapFrames)) { this.mapFrames[seqId] = new BeginFrameInfo(seqId, startTime, isDropped, isPartial); this.queueFrames.push(seqId); } } // Set a BeginFrame in queue as dropped. setDropped(seqId: number, isDropped: boolean): void { if (seqId in this.mapFrames) { this.mapFrames[seqId].isDropped = isDropped; } } setPartial(seqId: number, isPartial: boolean): void { if (seqId in this.mapFrames) { this.mapFrames[seqId].isPartial = isPartial; } } processPendingBeginFramesOnDrawFrame(seqId: number): BeginFrameInfo[] { const framesToVisualize: BeginFrameInfo[] = []; // Do not visualize this frame in the rare case where the current DrawFrame // does not have a corresponding BeginFrame. if (seqId in this.mapFrames) { // Pop all BeginFrames before the current frame, and add only the dropped // ones in |frames_to_visualize|. // Non-dropped frames popped here are BeginFrames that are never // drawn (but not considered dropped either for some reason). // Those frames do not require an proactive visualization effort and will // be naturally presented as continuationss of other frames. while (this.queueFrames[0] !== seqId) { const currentSeqId = this.queueFrames[0]; if (this.mapFrames[currentSeqId].isDropped) { framesToVisualize.push(this.mapFrames[currentSeqId]); } delete this.mapFrames[currentSeqId]; this.queueFrames.shift(); } // Pop the BeginFrame associated with the current DrawFrame. framesToVisualize.push(this.mapFrames[seqId]); delete this.mapFrames[seqId]; this.queueFrames.shift(); } return framesToVisualize; } } export function framesWithinWindow( frames: readonly Types.Events.LegacyTimelineFrame[], startTime: Types.Timing.Micro, endTime: Types.Timing.Micro): Types.Events.LegacyTimelineFrame[] { const firstFrame = Platform.ArrayUtilities.lowerBound(frames, startTime || 0, (time, frame) => time - frame.endTime); const lastFrame = Platform.ArrayUtilities.lowerBound(frames, endTime || Infinity, (time, frame) => time - frame.startTime); return frames.slice(firstFrame, lastFrame); }