UNPKG

chrome-devtools-frontend

Version:
635 lines (562 loc) • 22 kB
/* * Copyright (C) 2013 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. */ /* eslint-disable @typescript-eslint/naming-convention */ import * as Platform from '../../core/platform/platform.js'; import * as SDK from '../../core/sdk/sdk.js'; import type * as Protocol from '../../generated/protocol.js'; import * as TraceEngine from '../trace/trace.js'; import {RecordType, EventOnTimelineData} from './TimelineModel.js'; import {TracingLayerTree, type TracingLayerPayload, type TracingLayerTile} from './TracingLayerTree.js'; export class TimelineFrameModel { private readonly categoryMapper: (arg0: SDK.TracingModel.Event) => string; private frames!: TimelineFrame[]; private frameById!: { [x: number]: TimelineFrame, }; private beginFrameQueue!: TimelineFrameBeginFrameQueue; private minimumRecordTime!: number; private lastFrame!: TimelineFrame|null; private mainFrameCommitted!: boolean; private mainFrameRequested!: boolean; private lastLayerTree!: TracingFrameLayerTree|null; private framePendingActivation!: PendingFrame|null; private currentTaskTimeByCategory!: { [x: string]: number, }; private target!: SDK.Target.Target|null; private framePendingCommit?: PendingFrame|null; private lastBeginFrame?: number|null; private lastDroppedFrame?: number|null; private lastNeedsBeginFrame?: number|null; private lastTaskBeginTime?: number|null; private layerTreeId?: number|null; private currentProcessMainThread?: SDK.TracingModel.Thread|null; constructor(categoryMapper: (arg0: SDK.TracingModel.Event) => string) { this.categoryMapper = categoryMapper; this.reset(); } getFrames(): TimelineFrame[] { return this.frames; } getFramesWithinWindow(startTime: number, endTime: number): TimelineFrame[] { const firstFrame = Platform.ArrayUtilities.lowerBound(this.frames, startTime || 0, (time, frame) => time - frame.endTime); const lastFrame = Platform.ArrayUtilities.lowerBound(this.frames, endTime || Infinity, (time, frame) => time - frame.startTime); return this.frames.slice(firstFrame, lastFrame); } hasRasterTile(rasterTask: SDK.TracingModel.Event): boolean { const data = rasterTask.args['tileData']; if (!data) { return false; } const frameId = data['sourceFrameNumber']; const frame = frameId && this.frameById[frameId]; if (!frame || !frame.layerTree) { return false; } return true; } rasterTilePromise(rasterTask: SDK.TracingModel.Event): Promise<{ rect: Protocol.DOM.Rect, snapshot: SDK.PaintProfiler.PaintProfilerSnapshot, }|null> { if (!this.target) { return Promise.resolve(null); } const data = rasterTask.args['tileData']; const frameId = (data['sourceFrameNumber'] as number); const tileId = data['tileId'] && data['tileId']['id_ref']; const frame = frameId && this.frameById[frameId]; if (!frame || !frame.layerTree || !tileId) { return Promise.resolve(null); } return frame.layerTree.layerTreePromise().then(layerTree => layerTree && layerTree.pictureForRasterTile(tileId)); } reset(): void { this.minimumRecordTime = Infinity; this.frames = []; this.frameById = {}; this.beginFrameQueue = new TimelineFrameBeginFrameQueue(); this.lastFrame = null; this.lastLayerTree = null; this.mainFrameCommitted = false; this.mainFrameRequested = false; this.framePendingCommit = null; this.lastBeginFrame = null; this.lastDroppedFrame = null; this.lastNeedsBeginFrame = null; this.framePendingActivation = null; this.lastTaskBeginTime = null; this.target = null; this.layerTreeId = null; this.currentTaskTimeByCategory = {}; } handleBeginFrame(startTime: number, seqId: number): void { if (!this.lastFrame) { this.startFrame(startTime); } this.lastBeginFrame = startTime; this.beginFrameQueue.addFrameIfNotExists(seqId, startTime, false, false); } handleDroppedFrame(startTime: number, seqId: number, isPartial: boolean): void { if (!this.lastFrame) { this.startFrame(startTime); } // 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: number, seqId: number): void { if (!this.lastFrame) { this.startFrame(startTime); 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); 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: TracingFrameLayerTree): void { this.lastLayerTree = layerTree; } handleNeedFrameChanged(startTime: number, needsBeginFrame: boolean): void { if (needsBeginFrame) { this.lastNeedsBeginFrame = startTime; } } private startFrame(startTime: number): void { if (this.lastFrame) { this.flushFrame(this.lastFrame, startTime); } this.lastFrame = new TimelineFrame(startTime, startTime - this.minimumRecordTime); } private flushFrame(frame: TimelineFrame, endTime: number): void { frame.setLayerTree(this.lastLayerTree); frame.setEndTime(endTime); if (this.lastLayerTree) { this.lastLayerTree.setPaints(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})`); } this.frames.push(frame); if (typeof frame.mainFrameId === 'number') { this.frameById[frame.mainFrameId] = frame; } } private commitPendingFrame(): void { if (!this.framePendingActivation || !this.lastFrame) { return; } this.lastFrame.addTimeForCategories(this.framePendingActivation.timeByCategory); this.lastFrame.paints = this.framePendingActivation.paints; this.lastFrame.mainFrameId = this.framePendingActivation.mainFrameId; this.framePendingActivation = null; } addTraceEvents(target: SDK.Target.Target|null, events: SDK.TracingModel.Event[], threadData: { thread: SDK.TracingModel.Thread, time: number, }[]): void { this.target = target; let j = 0; this.currentProcessMainThread = threadData.length && threadData[0].thread || null; for (let i = 0; i < events.length; ++i) { while (j + 1 < threadData.length && threadData[j + 1].time <= events[i].startTime) { this.currentProcessMainThread = threadData[++j].thread; } this.addTraceEvent(events[i]); } this.currentProcessMainThread = null; } private addTraceEvent(event: SDK.TracingModel.Event): void { if (event.startTime && event.startTime < this.minimumRecordTime) { this.minimumRecordTime = event.startTime; } if (event.name === RecordType.SetLayerTreeId) { this.layerTreeId = event.args['layerTreeId'] || event.args['data']['layerTreeId']; } else if ( event.id && event.phase === TraceEngine.Types.TraceEvents.Phase.OBJECT_SNAPSHOT && event.name === RecordType.LayerTreeHostImplSnapshot && Number(event.id) === this.layerTreeId && this.target) { const snapshot = (event as SDK.TracingModel.ObjectSnapshot); this.handleLayerTreeSnapshot(new TracingFrameLayerTree(this.target, snapshot)); } else { this.processCompositorEvents(event); if (event.thread === this.currentProcessMainThread) { this.addMainThreadTraceEvent(event); } else if (this.lastFrame && event.selfTime && !SDK.TracingModel.TracingModel.isTopLevelEvent(event)) { this.lastFrame.addTimeForCategory(this.categoryMapper(event), event.selfTime); } } } private processCompositorEvents(event: SDK.TracingModel.Event): void { if (event.args['layerTreeId'] !== this.layerTreeId) { return; } const timestamp = event.startTime; if (event.name === RecordType.BeginFrame) { this.handleBeginFrame(timestamp, event.args['frameSeqId']); } else if (event.name === RecordType.DrawFrame) { this.handleDrawFrame(timestamp, event.args['frameSeqId']); } else if (event.name === RecordType.ActivateLayerTree) { this.handleActivateLayerTree(); } else if (event.name === RecordType.RequestMainThreadFrame) { this.handleRequestMainThreadFrame(); } else if (event.name === RecordType.NeedsBeginFrameChanged) { this.handleNeedFrameChanged(timestamp, event.args['data'] && event.args['data']['needsBeginFrame']); } else if (event.name === RecordType.DroppedFrame) { this.handleDroppedFrame(timestamp, event.args['frameSeqId'], event.args['hasPartialUpdate']); } } private addMainThreadTraceEvent(event: SDK.TracingModel.Event): void { if (SDK.TracingModel.TracingModel.isTopLevelEvent(event)) { this.currentTaskTimeByCategory = {}; this.lastTaskBeginTime = event.startTime; } if (!this.framePendingCommit && TimelineFrameModel.mainFrameMarkers.indexOf(event.name as RecordType) >= 0) { this.framePendingCommit = new PendingFrame(this.lastTaskBeginTime || event.startTime, this.currentTaskTimeByCategory); } if (!this.framePendingCommit) { this.addTimeForCategory(this.currentTaskTimeByCategory, event); return; } this.addTimeForCategory(this.framePendingCommit.timeByCategory, event); if (event.name === RecordType.BeginMainThreadFrame && event.args['data'] && event.args['data']['frameId']) { this.framePendingCommit.mainFrameId = event.args['data']['frameId']; } if (event.name === RecordType.Paint && event.args['data']['layerId'] && EventOnTimelineData.forEvent(event).picture && this.target) { this.framePendingCommit.paints.push(new LayerPaintEvent(event, this.target)); } // Commit will be replacing CompositeLayers but CompositeLayers is kept // around for backwards compatibility. if ((event.name === RecordType.CompositeLayers || event.name === RecordType.Commit) && event.args['layerTreeId'] === this.layerTreeId) { this.handleCommit(); } } private addTimeForCategory( timeByCategory: { [x: string]: number, }, event: SDK.TracingModel.Event): void { if (!event.selfTime) { return; } const categoryName = this.categoryMapper(event); timeByCategory[categoryName] = (timeByCategory[categoryName] || 0) + event.selfTime; } private static readonly mainFrameMarkers: RecordType[] = [ RecordType.ScheduleStyleRecalculation, RecordType.InvalidateLayout, RecordType.BeginMainThreadFrame, RecordType.ScrollLayer, ]; } export class TracingFrameLayerTree { private readonly target: SDK.Target.Target; private readonly snapshot: SDK.TracingModel.ObjectSnapshot; private paintsInternal!: LayerPaintEvent[]|undefined; constructor(target: SDK.Target.Target, snapshot: SDK.TracingModel.ObjectSnapshot) { this.target = target; this.snapshot = snapshot; } async layerTreePromise(): Promise<TracingLayerTree|null> { const result = (await this.snapshot.objectPromise() as unknown as { active_tiles: TracingLayerTile[], device_viewport_size: { width: number, height: number, }, active_tree: { root_layer: TracingLayerPayload, layers: TracingLayerPayload[], }, }); if (!result) { return null; } const viewport = result['device_viewport_size']; const tiles = result['active_tiles']; const rootLayer = result['active_tree']['root_layer']; const layers = result['active_tree']['layers']; const layerTree = new TracingLayerTree(this.target); layerTree.setViewportSize(viewport); layerTree.setTiles(tiles); await layerTree.setLayers(rootLayer, layers, this.paintsInternal || []); return layerTree; } paints(): LayerPaintEvent[] { return this.paintsInternal || []; } setPaints(paints: LayerPaintEvent[]): void { this.paintsInternal = paints; } } export class TimelineFrame { startTime: number; startTimeOffset: number; endTime: number; duration: number; timeByCategory: { [x: string]: number, }; cpuTime: number; idle: boolean; dropped: boolean; isPartial: boolean; layerTree: TracingFrameLayerTree|null; paints: LayerPaintEvent[]; mainFrameId: number|undefined; constructor(startTime: number, startTimeOffset: number) { this.startTime = startTime; this.startTimeOffset = startTimeOffset; this.endTime = this.startTime; this.duration = 0; this.timeByCategory = {}; this.cpuTime = 0; this.idle = false; this.dropped = false; this.isPartial = false; this.layerTree = null; this.paints = []; this.mainFrameId = undefined; } hasWarnings(): boolean { return false; } setEndTime(endTime: number): void { this.endTime = endTime; this.duration = this.endTime - this.startTime; } setLayerTree(layerTree: TracingFrameLayerTree|null): void { this.layerTree = layerTree; } addTimeForCategories(timeByCategory: { [x: string]: number, }): void { for (const category in timeByCategory) { this.addTimeForCategory(category, timeByCategory[category]); } } addTimeForCategory(category: string, time: number): void { this.timeByCategory[category] = (this.timeByCategory[category] || 0) + time; this.cpuTime += time; } } export class LayerPaintEvent { private readonly eventInternal: SDK.TracingModel.Event; private readonly target: SDK.Target.Target|null; constructor(event: SDK.TracingModel.Event, target: SDK.Target.Target|null) { this.eventInternal = event; this.target = target; } layerId(): string { return this.eventInternal.args['data']['layerId']; } event(): SDK.TracingModel.Event { return this.eventInternal; } picturePromise(): Promise<{ rect: Array<number>, serializedPicture: string, }|null> { const picture = EventOnTimelineData.forEvent(this.eventInternal).picture; if (!picture) { return Promise.resolve(null); } // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) // eslint-disable-next-line @typescript-eslint/no-explicit-any return picture.objectPromise().then((result: any) => { if (!result) { return null; } const rect = result['params'] && result['params']['layer_rect']; const picture = result['skp64']; return rect && picture ? {rect: rect, serializedPicture: picture} : null; }); } async snapshotPromise(): Promise<{ rect: Array<number>, snapshot: SDK.PaintProfiler.PaintProfilerSnapshot, }|null> { const paintProfilerModel = this.target && this.target.model(SDK.PaintProfiler.PaintProfilerModel); const picture = await this.picturePromise(); if (!picture || !paintProfilerModel) { return null; } const snapshot = await paintProfilerModel.loadSnapshot(picture.serializedPicture); return snapshot ? {rect: picture.rect, snapshot: snapshot} : null; } } export class PendingFrame { timeByCategory: { [x: string]: number, }; paints: LayerPaintEvent[]; mainFrameId: number|undefined; triggerTime: number; constructor(triggerTime: number, timeByCategory: { [x: string]: number, }) { this.timeByCategory = timeByCategory; this.paints = []; this.mainFrameId = undefined; this.triggerTime = triggerTime; } } // The parameters of an impl-side BeginFrame. class BeginFrameInfo { seqId: number; startTime: number; isDropped: boolean; isPartial: boolean; constructor(seqId: number, startTime: number, 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!: { [x: number]: BeginFrameInfo, }; constructor() { this.queueFrames = []; this.mapFrames = {}; } // Add a BeginFrame to the queue, if it does not already exit. addFrameIfNotExists(seqId: number, startTime: number, 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; } }