UNPKG

chrome-devtools-frontend

Version:
991 lines (916 loc) • 33.5 kB
// Copyright 2022 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 SDK from '../core/sdk/sdk.js'; import type * as Protocol from '../generated/protocol.js'; import * as Bindings from '../models/bindings/bindings.js'; import * as CPUProfile from '../models/cpu_profile/cpu_profile.js'; import * as Trace from '../models/trace/trace.js'; import * as Workspace from '../models/workspace/workspace.js'; import * as Timeline from '../panels/timeline/timeline.js'; import * as PerfUI from '../ui/legacy/components/perf_ui/perf_ui.js'; import * as UI from '../ui/legacy/legacy.js'; import {raf, renderElementIntoDOM} from './DOMHelpers.js'; import {initializeGlobalVars} from './EnvironmentHelpers.js'; import {TraceLoader} from './TraceLoader.js'; // This mock class is used for instancing a flame chart in the helpers. // Its implementation is empty because the methods aren't used by the // helpers, only the mere definition. export class MockFlameChartDelegate implements PerfUI.FlameChart.FlameChartDelegate { windowChanged(_startTime: number, _endTime: number, _animate: boolean): void { } updateRangeSelection(_startTime: number, _endTime: number): void { } updateSelectedGroup(_flameChart: PerfUI.FlameChart.FlameChart, _group: PerfUI.FlameChart.Group|null): void { } } /** * @deprecated this will be removed once we have migrated from interaction tests for screenshots. Please use `renderFlameChartIntoDOM`. * * Draws a set of tracks track in the flame chart using the new system. * For this to work, every track that will be rendered must have a * corresponding track appender registered in the * CompatibilityTracksAppender. * * @param context The unit test context. * @param traceFileName The name of the trace file to be loaded into the * flame chart. * @param trackAppenderNames A Set with the names of the tracks to be * rendered. For example, Set("Timings"). * @param expanded whether the track should be expanded * @param trackName optional param to filter tracks by their name. * @returns a flame chart element and its corresponding data provider. */ export async function getMainFlameChartWithTracks( context: Mocha.Context|null, traceFileName: string, trackAppenderNames: Set<Timeline.CompatibilityTracksAppender.TrackAppenderName>, expanded: boolean, trackName?: string): Promise<{ flameChart: PerfUI.FlameChart.FlameChart, dataProvider: Timeline.TimelineFlameChartDataProvider.TimelineFlameChartDataProvider, }> { await initializeGlobalVars(); // This function is used to load a component example. const {parsedTrace} = await TraceLoader.traceEngine(context, traceFileName); const entityMapper = new Timeline.Utils.EntityMapper.EntityMapper(parsedTrace); const dataProvider = new Timeline.TimelineFlameChartDataProvider.TimelineFlameChartDataProvider(); dataProvider.setModel(parsedTrace, entityMapper); const tracksAppender = dataProvider.compatibilityTracksAppenderInstance(); tracksAppender.setVisibleTracks(trackAppenderNames); dataProvider.buildWithCustomTracksForTest( {filterTracks: name => trackName ? name.includes(trackName) : true, expandTracks: _ => expanded}); const delegate = new MockFlameChartDelegate(); const flameChart = new PerfUI.FlameChart.FlameChart(dataProvider, delegate); const minTime = Trace.Helpers.Timing.microToMilli(parsedTrace.Meta.traceBounds.min); const maxTime = Trace.Helpers.Timing.microToMilli(parsedTrace.Meta.traceBounds.max); flameChart.setWindowTimes(minTime, maxTime); flameChart.markAsRoot(); flameChart.update(); return {flameChart, dataProvider}; } export interface RenderFlameChartOptions { dataProvider: 'MAIN'|'NETWORK'; /** * The trace file to import. You must include `.json.gz` at the end of the file name. * Alternatively, you can provide the actual file. This is useful only if you * are providing a mocked file; generally you should prefer to pass the file * name so that the TraceLoader can take care of loading and caching the * trace. */ traceFile: string|Trace.Handlers.Types.ParsedTrace; /** * Filter the tracks that will be rendered by their name. The name here is * the user visible name that is drawn onto the flame chart. */ filterTracks?: (trackName: string, trackIndex: number) => boolean; /** * Choose which track(s) that have been drawn should be expanded. The name * here is the user visible name that is drawn onto the flame chart. */ expandTracks?: (trackName: string, trackIndex: number) => boolean; customStartTime?: Trace.Types.Timing.Milli; customEndTime?: Trace.Types.Timing.Milli; /** * A custom height in pixels. By default a height is chosen that will * vertically fit the entire FlameChart. * (calculated based on the pixel offset of the last visible track.) */ customHeight?: number; /** * When the frames track renders screenshots, we do so async, as we have to * fetch screenshots first to draw them. If this flag is `true`, we block and * preload all the screenshots before rendering, thus making it faster in a * test to expand the frames track as it can be done with no async calls to * fetch images. */ preloadScreenshots?: boolean; } /** * Renders a flame chart into the unit test DOM that renders a real provided * trace file. * It will take care of all the setup and configuration for you. */ export async function renderFlameChartIntoDOM(context: Mocha.Context|null, options: RenderFlameChartOptions): Promise<{ flameChart: PerfUI.FlameChart.FlameChart, dataProvider: Timeline.TimelineFlameChartDataProvider.TimelineFlameChartDataProvider | Timeline.TimelineFlameChartNetworkDataProvider.TimelineFlameChartNetworkDataProvider, target: HTMLElement, parsedTrace: Trace.Handlers.Types.ParsedTrace, }> { const targetManager = SDK.TargetManager.TargetManager.instance({forceNew: true}); const workspace = Workspace.Workspace.WorkspaceImpl.instance({forceNew: true}); const resourceMapping = new Bindings.ResourceMapping.ResourceMapping(targetManager, workspace); const debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance({ forceNew: true, resourceMapping, targetManager, }); Bindings.IgnoreListManager.IgnoreListManager.instance({ forceNew: true, debuggerWorkspaceBinding, }); let parsedTrace: Trace.Handlers.Types.ParsedTrace|null = null; if (typeof options.traceFile === 'string') { parsedTrace = (await TraceLoader.traceEngine(context, options.traceFile)).parsedTrace; } else { parsedTrace = options.traceFile; } if (options.preloadScreenshots) { await Timeline.Utils.ImageCache.preload(parsedTrace.Screenshots.screenshots ?? []); } const entityMapper = new Timeline.Utils.EntityMapper.EntityMapper(parsedTrace); const dataProvider = options.dataProvider === 'MAIN' ? new Timeline.TimelineFlameChartDataProvider.TimelineFlameChartDataProvider() : new Timeline.TimelineFlameChartNetworkDataProvider.TimelineFlameChartNetworkDataProvider(); dataProvider.setModel(parsedTrace, entityMapper); if (dataProvider instanceof Timeline.TimelineFlameChartDataProvider.TimelineFlameChartDataProvider) { dataProvider.buildWithCustomTracksForTest({ filterTracks: options.filterTracks, expandTracks: options.expandTracks, }); } else { // Calling this method triggers the data being generated & the Network appender being created + drawn. dataProvider.timelineData(); } const delegate = new MockFlameChartDelegate(); const flameChart = new PerfUI.FlameChart.FlameChart(dataProvider, delegate); const minTime = options.customStartTime ?? Trace.Helpers.Timing.microToMilli(parsedTrace.Meta.traceBounds.min); const maxTime = options.customEndTime ?? Trace.Helpers.Timing.microToMilli(parsedTrace.Meta.traceBounds.max); flameChart.setWindowTimes(minTime, maxTime); flameChart.markAsRoot(); const target = document.createElement('div'); target.innerHTML = `<style>${UI.inspectorCommonStyles}</style>`; const timingsTrackOffset = flameChart.levelToOffset(dataProvider.maxStackDepth()); // Allow an extra 10px so no scrollbar is shown if using the default height // that fits everything inside. const heightPixels = options.customHeight ?? timingsTrackOffset + 10; target.style.height = `${heightPixels}px`; target.style.display = 'flex'; target.style.width = '800px'; renderElementIntoDOM(target); flameChart.show(target); flameChart.update(); await raf(); return {flameChart, dataProvider, target, parsedTrace}; } /** * Draws the network track in the flame chart using the legacy system. * * @param traceFileName The name of the trace file to be loaded to the flame * chart. * @param expanded if the track is expanded * @returns a flame chart element and its corresponding data provider. */ export async function getNetworkFlameChart(traceFileName: string, expanded: boolean): Promise<{ flameChart: PerfUI.FlameChart.FlameChart, dataProvider: Timeline.TimelineFlameChartNetworkDataProvider.TimelineFlameChartNetworkDataProvider, }> { await initializeGlobalVars(); const {parsedTrace} = await TraceLoader.traceEngine(/* context= */ null, traceFileName); const entityMapper = new Timeline.Utils.EntityMapper.EntityMapper(parsedTrace); const minTime = Trace.Helpers.Timing.microToMilli(parsedTrace.Meta.traceBounds.min); const maxTime = Trace.Helpers.Timing.microToMilli(parsedTrace.Meta.traceBounds.max); const dataProvider = new Timeline.TimelineFlameChartNetworkDataProvider.TimelineFlameChartNetworkDataProvider(); dataProvider.setModel(parsedTrace, entityMapper); dataProvider.setWindowTimes(minTime, maxTime); dataProvider.timelineData().groups.forEach(group => { group.expanded = expanded; }); const delegate = new MockFlameChartDelegate(); const flameChart = new PerfUI.FlameChart.FlameChart(dataProvider, delegate); flameChart.setWindowTimes(minTime, maxTime); flameChart.markAsRoot(); flameChart.update(); return {flameChart, dataProvider}; } // We create here a cross-test base trace event. It is assumed that each // test will import this default event and copy-override properties at will. export const defaultTraceEvent: Trace.Types.Events.Event = { name: 'process_name', tid: Trace.Types.Events.ThreadID(0), pid: Trace.Types.Events.ProcessID(0), ts: Trace.Types.Timing.Micro(0), cat: 'test', ph: Trace.Types.Events.Phase.METADATA, }; /** * Gets the tree in a thread. * @see RendererHandler.ts */ export function getTree(thread: Trace.Handlers.ModelHandlers.Renderer.RendererThread): Trace.Helpers.TreeHelpers.TraceEntryTree { const tree = thread.tree; if (!tree) { assert(false, `Couldn't get tree in thread ${thread.name}`); } return tree; } /** * Gets the n-th root from a tree in a thread. * @see RendererHandler.ts */ export function getRootAt(thread: Trace.Handlers.ModelHandlers.Renderer.RendererThread, index: number): Trace.Helpers.TreeHelpers.TraceEntryNode { const tree = getTree(thread); const node = [...tree.roots][index]; if (node === undefined) { assert(false, `Couldn't get the id of the root at index ${index} in thread ${thread.name}`); } return node; } /** * Gets all nodes in a thread. To finish this task, we Walk through all the nodes, starting from the root node. */ export function getAllNodes(roots: Set<Trace.Helpers.TreeHelpers.TraceEntryNode>): Trace.Helpers.TreeHelpers.TraceEntryNode[] { const allNodes: Trace.Helpers.TreeHelpers.TraceEntryNode[] = []; const children: Trace.Helpers.TreeHelpers.TraceEntryNode[] = Array.from(roots); while (children.length > 0) { const childNode = children.shift(); if (childNode) { allNodes.push(childNode); children.push(...childNode.children); } } return allNodes; } /** * Gets the node with an id from a tree in a thread. * @see RendererHandler.ts */ export function getNodeFor( thread: Trace.Handlers.ModelHandlers.Renderer.RendererThread, nodeId: Trace.Helpers.TreeHelpers.TraceEntryNodeId): Trace.Helpers.TreeHelpers.TraceEntryNode { const tree = getTree(thread); function findNode( nodes: Set<Trace.Helpers.TreeHelpers.TraceEntryNode>|Trace.Helpers.TreeHelpers.TraceEntryNode[], nodeId: Trace.Helpers.TreeHelpers.TraceEntryNodeId): Trace.Helpers.TreeHelpers.TraceEntryNode|undefined { for (const node of nodes) { const event = node.entry; if (Trace.Types.Events.isProfileCall(event) && event.nodeId === nodeId) { return node; } return findNode(node.children, nodeId); } return undefined; } const node = findNode(tree.roots, nodeId); if (!node) { assert(false, `Couldn't get the node with id ${nodeId} in thread ${thread.name}`); } return node; } /** * Gets all the `events` for the `nodes`. */ export function getEventsIn(nodes: IterableIterator<Trace.Helpers.TreeHelpers.TraceEntryNode>): Trace.Types.Events.Event[] { return [...nodes].flatMap(node => node ? node.entry : []); } /** * Pretty-prints a tree. */ export function prettyPrint( tree: Trace.Helpers.TreeHelpers.TraceEntryTree, predicate: (node: Trace.Helpers.TreeHelpers.TraceEntryNode, event: Trace.Types.Events.Event) => boolean = () => true, indentation = 2, delimiter = ' ', prefix = '-', newline = '\n', out = ''): string { let skipped = false; return printNodes(tree.roots); function printNodes(nodes: Set<Trace.Helpers.TreeHelpers.TraceEntryNode>|Trace.Helpers.TreeHelpers.TraceEntryNode[]): string { for (const node of nodes) { const event = node.entry; if (!predicate(node, event)) { out += `${!skipped ? newline : ''}.`; skipped = true; continue; } skipped = false; const spacing = new Array(node.depth * indentation).fill(delimiter).join(''); const eventType = Trace.Types.Events.isDispatch(event) ? `(${event.args.data?.type})` : false; const jsFunctionName = Trace.Types.Events.isProfileCall(event) ? `(${event.callFrame.functionName || 'anonymous'})` : false; const duration = `[${(event.dur || 0) / 1000}ms]`; const info = [jsFunctionName, eventType, duration].filter(Boolean); out += `${newline}${spacing}${prefix}${event.name} ${info.join(' ')}`; out = printNodes(node.children); } return out; } } /** * Builds a mock Complete. */ export function makeCompleteEvent( name: string, ts: number, dur: number, cat = '*', pid = 0, tid = 0): Trace.Types.Events.Complete { return { args: {}, cat, name, ph: Trace.Types.Events.Phase.COMPLETE, pid: Trace.Types.Events.ProcessID(pid), tid: Trace.Types.Events.ThreadID(tid), ts: Trace.Types.Timing.Micro(ts), dur: Trace.Types.Timing.Micro(dur), }; } export function makeAsyncStartEvent( name: string, ts: number, pid = 0, tid = 0, ): Trace.Types.Events.Async { return { args: {}, cat: '*', name, ph: Trace.Types.Events.Phase.ASYNC_NESTABLE_START, pid: Trace.Types.Events.ProcessID(pid), tid: Trace.Types.Events.ThreadID(tid), ts: Trace.Types.Timing.Micro(ts), }; } export function makeAsyncEndEvent( name: string, ts: number, pid = 0, tid = 0, ): Trace.Types.Events.Async { return { args: {}, cat: '*', name, ph: Trace.Types.Events.Phase.ASYNC_NESTABLE_END, pid: Trace.Types.Events.ProcessID(pid), tid: Trace.Types.Events.ThreadID(tid), ts: Trace.Types.Timing.Micro(ts), }; } /** * Builds a mock flow phase event. */ export function makeFlowPhaseEvent( name: string, ts: number, cat = '*', ph: Trace.Types.Events.Phase.FLOW_START|Trace.Types.Events.Phase.FLOW_END|Trace.Types.Events.Phase.FLOW_STEP, id = 0, pid = 0, tid = 0): Trace.Types.Events.FlowEvent { return { args: {}, cat, name, id, ph, pid: Trace.Types.Events.ProcessID(pid), tid: Trace.Types.Events.ThreadID(tid), ts: Trace.Types.Timing.Micro(ts), dur: Trace.Types.Timing.Micro(0), }; } /** * Builds flow phase events for a list of events belonging to the same * flow. `events` must be ordered. */ export function makeFlowEvents(events: Trace.Types.Events.Event[], flowId = 0): Trace.Types.Events.FlowEvent[] { const firstEvent = events.at(0); const lastEvent = events.at(-1); if (!lastEvent || !firstEvent) { return []; } const flowName = firstEvent.name; const flowStart = makeFlowPhaseEvent( flowName, firstEvent.ts, firstEvent.cat, Trace.Types.Events.Phase.FLOW_START, flowId, firstEvent.pid, firstEvent.tid); const flowEnd = makeFlowPhaseEvent( flowName, lastEvent.ts, lastEvent.cat, Trace.Types.Events.Phase.FLOW_END, flowId, lastEvent.pid, lastEvent.tid); const flowSteps: Trace.Types.Events.FlowEvent[] = []; for (let i = 1; i < events.length - 1; i++) { flowSteps.push(makeFlowPhaseEvent( flowName, events[i].ts, events[i].cat, Trace.Types.Events.Phase.FLOW_STEP, flowId, events[i].pid, events[i].tid)); } return [flowStart, ...flowSteps, flowEnd]; } /** * Builds a mock Instant. */ export function makeInstantEvent( name: string, tsMicroseconds: number, cat = '', pid = 0, tid = 0, s: Trace.Types.Events.Scope = Trace.Types.Events.Scope.THREAD): Trace.Types.Events.Instant { return { args: {}, cat, name, ph: Trace.Types.Events.Phase.INSTANT, pid: Trace.Types.Events.ProcessID(pid), tid: Trace.Types.Events.ThreadID(tid), ts: Trace.Types.Timing.Micro(tsMicroseconds), s, }; } /** * Builds a mock Begin. */ export function makeBeginEvent(name: string, ts: number, cat = '*', pid = 0, tid = 0): Trace.Types.Events.Begin { return { args: {}, cat, name, ph: Trace.Types.Events.Phase.BEGIN, pid: Trace.Types.Events.ProcessID(pid), tid: Trace.Types.Events.ThreadID(tid), ts: Trace.Types.Timing.Micro(ts), }; } /** * Builds a mock End. */ export function makeEndEvent(name: string, ts: number, cat = '*', pid = 0, tid = 0): Trace.Types.Events.End { return { args: {}, cat, name, ph: Trace.Types.Events.Phase.END, pid: Trace.Types.Events.ProcessID(pid), tid: Trace.Types.Events.ThreadID(tid), ts: Trace.Types.Timing.Micro(ts), }; } export function makeProfileCall( functionName: string, tsUs: number, durUs: number, pid = 0, tid = 0, nodeId = 0, url = ''): Trace.Types.Events.SyntheticProfileCall { return { cat: '', name: 'ProfileCall', nodeId, sampleIndex: 0, profileId: Trace.Types.Events.ProfileID('fake-profile-id'), ph: Trace.Types.Events.Phase.COMPLETE, pid: Trace.Types.Events.ProcessID(pid), tid: Trace.Types.Events.ThreadID(tid), ts: Trace.Types.Timing.Micro(tsUs), dur: Trace.Types.Timing.Micro(durUs), callFrame: { functionName, scriptId: '' as Protocol.Runtime.ScriptId, url, lineNumber: -1, columnNumber: -1, }, args: {}, }; } export const DevToolsTimelineCategory = 'disabled-by-default-devtools.timeline'; /** * Mocks an object compatible with the return type of the * RendererHandler using only an array of ordered entries. */ export function makeMockRendererHandlerData( entries: Trace.Types.Events.Event[], pid = 1, tid = 1): Trace.Handlers.ModelHandlers.Renderer.RendererHandlerData { const {tree, entryToNode} = Trace.Helpers.TreeHelpers.treify(entries, {filter: {has: () => true}}); const mockThread: Trace.Handlers.ModelHandlers.Renderer.RendererThread = { tree, name: 'thread', entries, profileCalls: entries.filter(Trace.Types.Events.isProfileCall), layoutEvents: entries.filter(Trace.Types.Events.isLayout), updateLayoutTreeEvents: entries.filter(Trace.Types.Events.isUpdateLayoutTree), }; const mockProcess: Trace.Handlers.ModelHandlers.Renderer.RendererProcess = { url: 'url', isOnMainFrame: true, threads: new Map([[tid as Trace.Types.Events.ThreadID, mockThread]]), }; const renderereEvents: Trace.Types.Events.RendererEvent[] = []; for (const entry of entries) { if (Trace.Types.Events.isRendererEvent(entry)) { renderereEvents.push(entry); } } return { processes: new Map([[pid as Trace.Types.Events.ProcessID, mockProcess]]), compositorTileWorkers: new Map(), entryToNode, allTraceEntries: renderereEvents, entityMappings: { entityByEvent: new Map(), eventsByEntity: new Map(), createdEntityCache: new Map(), }, }; } /** * Mocks an object compatible with the return type of the * SamplesHandler using only an array of ordered profile calls. */ export function makeMockSamplesHandlerData(profileCalls: Trace.Types.Events.SyntheticProfileCall[]): Trace.Handlers.ModelHandlers.Samples.SamplesHandlerData { const {tree, entryToNode} = Trace.Helpers.TreeHelpers.treify(profileCalls, {filter: {has: () => true}}); const profile: Protocol.Profiler.Profile = { nodes: [], startTime: profileCalls.at(0)?.ts || Trace.Types.Timing.Micro(0), endTime: profileCalls.at(-1)?.ts || Trace.Types.Timing.Micro(10e5), samples: [], timeDeltas: [], }; const nodesIds = new Map<number, Protocol.Profiler.ProfileNode>(); const lastTimestamp = profile.startTime; for (const profileCall of profileCalls) { let node = nodesIds.get(profileCall.nodeId); if (!node) { node = { id: profileCall.nodeId, callFrame: profileCall.callFrame, }; profile.nodes.push(node); nodesIds.set(profileCall.nodeId, node); } profile.samples?.push(node.id); const timeDelta = profileCall.ts - lastTimestamp; profile.timeDeltas?.push(timeDelta); } const profileData = { rawProfile: profile, parsedProfile: new CPUProfile.CPUProfileDataModel.CPUProfileDataModel(profile), profileCalls, profileTree: tree, profileId: Trace.Types.Events.ProfileID('fake-profile-id'), }; const profilesInThread = new Map([[1 as Trace.Types.Events.ThreadID, profileData]]); return { profilesInProcess: new Map([[1 as Trace.Types.Events.ProcessID, profilesInThread]]), entryToNode, }; } export function makeMockEntityData(events: Trace.Types.Events.Event[]): Trace.Handlers.Helpers.EntityMappings { const eventsByEntity = new Map<Trace.Handlers.Helpers.Entity, Trace.Types.Events.Event[]>(); const entityByEvent = new Map<Trace.Types.Events.Event, Trace.Handlers.Helpers.Entity>(); const createdEntityCache = new Map<string, Trace.Handlers.Helpers.Entity>(); events.forEach(event => { const entity = Trace.Handlers.Helpers.getEntityForEvent(event, createdEntityCache); if (!entity) { return; } if (eventsByEntity.has(entity)) { const events = eventsByEntity.get(entity) ?? []; events?.push(event); } else { eventsByEntity.set(entity, [event]); } entityByEvent.set(event, entity); }); return {eventsByEntity, entityByEvent, createdEntityCache}; } export class FakeFlameChartProvider implements PerfUI.FlameChart.FlameChartDataProvider { minimumBoundary(): number { return 0; } hasTrackConfigurationMode(): boolean { return false; } totalTime(): number { return 100; } formatValue(value: number): string { return value.toString(); } maxStackDepth(): number { return 3; } preparePopoverElement(_entryIndex: number): Element|null { return null; } canJumpToEntry(_entryIndex: number): boolean { return false; } entryTitle(entryIndex: number): string|null { return `Entry ${entryIndex}`; } entryFont(_entryIndex: number): string|null { return null; } entryColor(entryIndex: number): string { return [ 'lightblue', 'lightpink', 'yellow', 'lightgray', 'lightgreen', 'lightsalmon', 'orange', 'pink', ][entryIndex % 8]; } decorateEntry(): boolean { return false; } forceDecoration(_entryIndex: number): boolean { return false; } textColor(_entryIndex: number): string { return 'black'; } timelineData(): PerfUI.FlameChart.FlameChartTimelineData|null { return PerfUI.FlameChart.FlameChartTimelineData.createEmpty(); } } export interface FlameChartWithFakeProviderOptions { windowTimes?: [number, number]; } /** * Renders a flame chart using a fake provider and mock delegate. * @param provider - The fake flame chart provider. * @param options - Optional parameters. Includes windowTimes, an array specifying the minimum and maximum window times. Defaults to [0, 100]. * @returns A promise that resolves when the flame chart is rendered. */ export async function renderFlameChartWithFakeProvider( provider: FakeFlameChartProvider, options?: FlameChartWithFakeProviderOptions, ): Promise<void> { const delegate = new MockFlameChartDelegate(); const flameChart = new PerfUI.FlameChart.FlameChart(provider, delegate); const [minWindowTime, maxWindowTime] = options?.windowTimes ?? [0, 100]; flameChart.setWindowTimes(minWindowTime, maxWindowTime); const lastTrackOffset = flameChart.levelToOffset(provider.maxStackDepth()); const target = document.createElement('div'); target.innerHTML = `<style>${UI.inspectorCommonStyles}</style>`; // Allow an extra 10px so no scrollbar is shown. target.style.height = `${lastTrackOffset + 10}px`; target.style.display = 'flex'; target.style.width = '800px'; renderElementIntoDOM(target); flameChart.markAsRoot(); flameChart.show(target); flameChart.update(); await raf(); } /** * Renders a widget into an element that has the right styling to be a VBox. * Useful as many of the Performance Panel elements are rendered like this and * need a parent that is flex + has a height & width in order to render * correctly for screenshot tests. */ export function renderWidgetInVbox(widget: UI.Widget.Widget, opts: { width?: number, height?: number, flexAuto?: boolean, } = {}): void { const target = document.createElement('div'); target.innerHTML = `<style>${UI.inspectorCommonStyles}</style>`; target.classList.add('vbox'); target.classList.toggle('flex-auto', Boolean(opts.flexAuto)); target.style.width = (opts.width ?? 800) + 'px'; target.style.height = (opts.height ?? 600) + 'px'; widget.markAsRoot(); widget.show(target); renderElementIntoDOM(target); } export function getMainThread(data: Trace.Handlers.ModelHandlers.Renderer.RendererHandlerData): Trace.Handlers.ModelHandlers.Renderer.RendererThread { let mainThread: Trace.Handlers.ModelHandlers.Renderer.RendererThread|null = null; for (const [, process] of data.processes) { for (const [, thread] of process.threads) { if (thread.name === 'CrRendererMain') { mainThread = thread; break; } } } if (!mainThread) { throw new Error('Could not find main thread.'); } return mainThread; } type ParsedTrace = Trace.Handlers.Types.ParsedTrace; export function getBaseTraceParseModelData(overrides: Partial<ParsedTrace> = {}): ParsedTrace { return { Animations: {animations: []}, AnimationFrames: { animationFrames: [], presentationForFrame: new Map(), }, DOMStats: { domStatsByFrameId: new Map(), }, LayoutShifts: { clusters: [], clustersByNavigationId: new Map(), sessionMaxScore: 0, clsWindowID: 0, prePaintEvents: [], layoutInvalidationEvents: [], scheduleStyleInvalidationEvents: [], styleRecalcInvalidationEvents: [], renderFrameImplCreateChildFrameEvents: [], domLoadingEvents: [], layoutImageUnsizedEvents: [], remoteFonts: [], scoreRecords: [], backendNodeIds: [], paintImageEvents: [], }, Meta: { traceBounds: { min: Trace.Types.Timing.Micro(0), max: Trace.Types.Timing.Micro(100), range: Trace.Types.Timing.Micro(100), }, browserProcessId: Trace.Types.Events.ProcessID(-1), browserThreadId: Trace.Types.Events.ThreadID(-1), gpuProcessId: Trace.Types.Events.ProcessID(-1), gpuThreadId: Trace.Types.Events.ThreadID(-1), threadsInProcess: new Map(), navigationsByFrameId: new Map(), navigationsByNavigationId: new Map(), finalDisplayUrlByNavigationId: new Map(), mainFrameId: '', mainFrameURL: '', rendererProcessesByFrame: new Map(), topLevelRendererIds: new Set(), frameByProcessId: new Map(), mainFrameNavigations: [], traceIsGeneric: false, processNames: new Map(), }, Renderer: { processes: new Map(), compositorTileWorkers: new Map(), entryToNode: new Map(), allTraceEntries: [], entityMappings: { entityByEvent: new Map(), eventsByEntity: new Map(), createdEntityCache: new Map(), }, }, Screenshots: { legacySyntheticScreenshots: [], screenshots: [], }, Samples: { entryToNode: new Map(), profilesInProcess: new Map(), }, PageLoadMetrics: {metricScoresByFrameId: new Map(), allMarkerEvents: []}, UserInteractions: { allEvents: [], interactionEvents: [], beginCommitCompositorFrameEvents: [], parseMetaViewportEvents: [], interactionEventsWithNoNesting: [], longestInteractionEvent: null, interactionsOverThreshold: new Set(), }, NetworkRequests: { byId: new Map(), eventToInitiator: new Map(), byOrigin: new Map(), byTime: [], webSocket: [], entityMappings: { entityByEvent: new Map(), eventsByEntity: new Map(), createdEntityCache: new Map(), }, linkPreconnectEvents: [], }, GPU: { mainGPUThreadTasks: [], }, UserTimings: { consoleTimings: [], performanceMarks: [], performanceMeasures: [], timestampEvents: [], measureTraceByTraceId: new Map(), }, LargestImagePaint: {lcpRequestByNavigationId: new Map()}, LargestTextPaint: new Map(), AuctionWorklets: { worklets: new Map(), }, ExtensionTraceData: { entryToNode: new Map(), extensionMarkers: [], extensionTrackData: [], syntheticConsoleEntriesForTimingsTrack: [], }, Frames: { frames: [], framesById: {}, }, ImagePainting: { paintImageByDrawLazyPixelRef: new Map(), paintImageForEvent: new Map(), paintImageEventForUrl: new Map(), }, Initiators: { eventToInitiator: new Map(), initiatorToEvents: new Map(), }, Invalidations: { invalidationCountForEvent: new Map(), invalidationsForEvent: new Map(), }, LayerTree: { paints: [], paintsToSnapshots: new Map(), snapshots: [], }, Memory: { updateCountersByProcess: new Map(), }, PageFrames: { frames: new Map(), }, SelectorStats: { dataForUpdateLayoutEvent: new Map(), }, Warnings: { perEvent: new Map(), perWarning: new Map(), }, Workers: { workerIdByThread: new Map(), workerSessionIdEvents: [], workerURLById: new Map(), }, Flows: { flows: [], }, AsyncJSCalls: { schedulerToRunEntryPoints: new Map(), asyncCallToScheduler: new Map(), runEntryPointToScheduler: new Map(), }, Scripts: { scripts: [], }, ...overrides, }; } /** * A helper that will query the given array of events and find the first event * matching the predicate. It will also assert that a match is found, which * saves the need to do that for every test. */ export function getEventOfType<T extends Trace.Types.Events.Event>( events: Trace.Types.Events.Event[], predicate: (e: Trace.Types.Events.Event) => e is T): T { const match = events.find(predicate); if (!match) { throw new Error('Failed to find matching event of type.'); } return match; } /** * The Performance Panel is integrated with the IgnoreListManager so in tests * that render a flame chart or a track appender, it needs to be setup to avoid * errors. */ export function setupIgnoreListManagerEnvironment(): { ignoreListManager: Bindings.IgnoreListManager.IgnoreListManager, } { const targetManager = SDK.TargetManager.TargetManager.instance({forceNew: true}); const workspace = Workspace.Workspace.WorkspaceImpl.instance({forceNew: true}); const resourceMapping = new Bindings.ResourceMapping.ResourceMapping(targetManager, workspace); const debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance({ forceNew: true, resourceMapping, targetManager, }); const ignoreListManager = Bindings.IgnoreListManager.IgnoreListManager.instance({ forceNew: true, debuggerWorkspaceBinding, }); return {ignoreListManager}; } export function microsecondsTraceWindow(min: number, max: number): Trace.Types.Timing.TraceWindowMicro { return Trace.Helpers.Timing.traceWindowFromMicroSeconds( min as Trace.Types.Timing.Micro, max as Trace.Types.Timing.Micro, ); } export function microseconds(x: number): Trace.Types.Timing.Micro { return Trace.Types.Timing.Micro(x); } export function milliseconds(x: number): Trace.Types.Timing.Milli { return Trace.Types.Timing.Milli(x); }