blob: 1e1028ca2178f8a8d369327db670a0120e621efe [file]
// 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 TraceEngine 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 {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 {
}
}
/**
* 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 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(
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 traceParsedData = await TraceLoader.traceEngine(/* context= */ null, traceFileName);
const dataProvider = new Timeline.TimelineFlameChartDataProvider.TimelineFlameChartDataProvider();
// The data provider still needs a reference to the legacy model to
// work properly.
dataProvider.setModel(traceParsedData);
const tracksAppender = dataProvider.compatibilityTracksAppenderInstance();
tracksAppender.setVisibleTracks(trackAppenderNames);
dataProvider.buildFromTrackAppenders(
{filterThreadsByName: trackName, expandedTracks: expanded ? trackAppenderNames : undefined});
const delegate = new MockFlameChartDelegate();
const flameChart = new PerfUI.FlameChart.FlameChart(dataProvider, delegate);
const minTime = TraceEngine.Helpers.Timing.microSecondsToMilliseconds(traceParsedData.Meta.traceBounds.min);
const maxTime = TraceEngine.Helpers.Timing.microSecondsToMilliseconds(traceParsedData.Meta.traceBounds.max);
flameChart.setWindowTimes(minTime, maxTime);
flameChart.markAsRoot();
flameChart.update();
return {flameChart, dataProvider};
}
/**
* 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 traceParsedData = await TraceLoader.traceEngine(/* context= */ null, traceFileName);
const minTime = TraceEngine.Helpers.Timing.microSecondsToMilliseconds(traceParsedData.Meta.traceBounds.min);
const maxTime = TraceEngine.Helpers.Timing.microSecondsToMilliseconds(traceParsedData.Meta.traceBounds.max);
const dataProvider = new Timeline.TimelineFlameChartNetworkDataProvider.TimelineFlameChartNetworkDataProvider();
dataProvider.setModel(traceParsedData);
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: TraceEngine.Types.TraceEvents.TraceEventData = {
name: 'process_name',
tid: TraceEngine.Types.TraceEvents.ThreadID(0),
pid: TraceEngine.Types.TraceEvents.ProcessID(0),
ts: TraceEngine.Types.Timing.MicroSeconds(0),
cat: 'test',
ph: TraceEngine.Types.TraceEvents.Phase.METADATA,
};
/**
* Gets the tree in a thread.
* @see RendererHandler.ts
*/
export function getTree(thread: TraceEngine.Handlers.ModelHandlers.Renderer.RendererThread):
TraceEngine.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: TraceEngine.Handlers.ModelHandlers.Renderer.RendererThread, index: number):
TraceEngine.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<TraceEngine.Helpers.TreeHelpers.TraceEntryNode>):
TraceEngine.Helpers.TreeHelpers.TraceEntryNode[] {
const allNodes: TraceEngine.Helpers.TreeHelpers.TraceEntryNode[] = [];
const children: TraceEngine.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: TraceEngine.Handlers.ModelHandlers.Renderer.RendererThread,
nodeId: TraceEngine.Helpers.TreeHelpers.TraceEntryNodeId): TraceEngine.Helpers.TreeHelpers.TraceEntryNode {
const tree = getTree(thread);
function findNode(
nodes: Set<TraceEngine.Helpers.TreeHelpers.TraceEntryNode>|TraceEngine.Helpers.TreeHelpers.TraceEntryNode[],
nodeId: TraceEngine.Helpers.TreeHelpers.TraceEntryNodeId): TraceEngine.Helpers.TreeHelpers.TraceEntryNode|
undefined {
for (const node of nodes) {
const event = node.entry;
if (TraceEngine.Types.TraceEvents.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<TraceEngine.Helpers.TreeHelpers.TraceEntryNode>):
TraceEngine.Types.TraceEvents.TraceEventData[] {
return [...nodes].flatMap(node => node ? node.entry : []);
}
/**
* Pretty-prints a tree.
*/
export function prettyPrint(
tree: TraceEngine.Helpers.TreeHelpers.TraceEntryTree,
predicate: (
node: TraceEngine.Helpers.TreeHelpers.TraceEntryNode,
event: TraceEngine.Types.TraceEvents.SyntheticTraceEntry) => boolean = () => true,
indentation: number = 2, delimiter: string = ' ', prefix: string = '-', newline: string = '\n',
out: string = ''): string {
let skipped = false;
return printNodes(tree.roots);
function printNodes(nodes: Set<TraceEngine.Helpers.TreeHelpers.TraceEntryNode>|
TraceEngine.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 =
TraceEngine.Types.TraceEvents.isTraceEventDispatch(event) ? `(${event.args.data?.type})` : false;
const jsFunctionName = TraceEngine.Types.TraceEvents.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 TraceEventComplete.
*/
export function makeCompleteEvent(
name: string, ts: number, dur: number, cat: string = '*', pid: number = 0,
tid: number = 0): TraceEngine.Types.TraceEvents.TraceEventComplete {
return {
args: {},
cat,
name,
ph: TraceEngine.Types.TraceEvents.Phase.COMPLETE,
pid: TraceEngine.Types.TraceEvents.ProcessID(pid),
tid: TraceEngine.Types.TraceEvents.ThreadID(tid),
ts: TraceEngine.Types.Timing.MicroSeconds(ts),
dur: TraceEngine.Types.Timing.MicroSeconds(dur),
};
}
export function makeAsyncStartEvent(
name: string,
ts: number,
pid: number = 0,
tid: number = 0,
): TraceEngine.Types.TraceEvents.TraceEventAsync {
return {
args: {},
cat: '*',
name,
ph: TraceEngine.Types.TraceEvents.Phase.ASYNC_NESTABLE_START,
pid: TraceEngine.Types.TraceEvents.ProcessID(pid),
tid: TraceEngine.Types.TraceEvents.ThreadID(tid),
ts: TraceEngine.Types.Timing.MicroSeconds(ts),
};
}
export function makeAsyncEndEvent(
name: string,
ts: number,
pid: number = 0,
tid: number = 0,
): TraceEngine.Types.TraceEvents.TraceEventAsync {
return {
args: {},
cat: '*',
name,
ph: TraceEngine.Types.TraceEvents.Phase.ASYNC_NESTABLE_END,
pid: TraceEngine.Types.TraceEvents.ProcessID(pid),
tid: TraceEngine.Types.TraceEvents.ThreadID(tid),
ts: TraceEngine.Types.Timing.MicroSeconds(ts),
};
}
export function makeCompleteEventInMilliseconds(
name: string, tsMillis: number, durMillis: number, cat: string = '*', pid: number = 0,
tid: number = 0): TraceEngine.Types.TraceEvents.TraceEventComplete {
return makeCompleteEvent(
name, TraceEngine.Helpers.Timing.millisecondsToMicroseconds(TraceEngine.Types.Timing.MilliSeconds(tsMillis)),
TraceEngine.Helpers.Timing.millisecondsToMicroseconds(TraceEngine.Types.Timing.MilliSeconds(durMillis)), cat, pid,
tid);
}
/**
* Builds a mock TraceEventInstant.
*/
export function makeInstantEvent(
name: string, ts: number, cat: string = '', pid: number = 0, tid: number = 0,
s: TraceEngine.Types.TraceEvents.TraceEventScope =
TraceEngine.Types.TraceEvents.TraceEventScope.THREAD): TraceEngine.Types.TraceEvents.TraceEventInstant {
return {
args: {},
cat,
name,
ph: TraceEngine.Types.TraceEvents.Phase.INSTANT,
pid: TraceEngine.Types.TraceEvents.ProcessID(pid),
tid: TraceEngine.Types.TraceEvents.ThreadID(tid),
ts: TraceEngine.Types.Timing.MicroSeconds(ts),
s,
};
}
/**
* Builds a mock TraceEventBegin.
*/
export function makeBeginEvent(name: string, ts: number, cat: string = '*', pid: number = 0, tid: number = 0):
TraceEngine.Types.TraceEvents.TraceEventBegin {
return {
args: {},
cat,
name,
ph: TraceEngine.Types.TraceEvents.Phase.BEGIN,
pid: TraceEngine.Types.TraceEvents.ProcessID(pid),
tid: TraceEngine.Types.TraceEvents.ThreadID(tid),
ts: TraceEngine.Types.Timing.MicroSeconds(ts),
};
}
/**
* Builds a mock TraceEventEnd.
*/
export function makeEndEvent(name: string, ts: number, cat: string = '*', pid: number = 0, tid: number = 0):
TraceEngine.Types.TraceEvents.TraceEventEnd {
return {
args: {},
cat,
name,
ph: TraceEngine.Types.TraceEvents.Phase.END,
pid: TraceEngine.Types.TraceEvents.ProcessID(pid),
tid: TraceEngine.Types.TraceEvents.ThreadID(tid),
ts: TraceEngine.Types.Timing.MicroSeconds(ts),
};
}
export function makeProfileCall(
functionName: string, tsMs: number, durMs: number,
pid: TraceEngine.Types.TraceEvents.ProcessID = TraceEngine.Types.TraceEvents.ProcessID(0),
tid: TraceEngine.Types.TraceEvents.ThreadID = TraceEngine.Types.TraceEvents.ThreadID(0), nodeId: number = 0,
url: string = ''): TraceEngine.Types.TraceEvents.SyntheticProfileCall {
return {
cat: '',
name: 'ProfileCall',
nodeId,
sampleIndex: 0,
profileId: TraceEngine.Types.TraceEvents.ProfileID('fake-profile-id'),
ph: TraceEngine.Types.TraceEvents.Phase.COMPLETE,
pid,
tid,
ts: TraceEngine.Types.Timing.MicroSeconds(tsMs),
dur: TraceEngine.Types.Timing.MicroSeconds(durMs),
selfTime: TraceEngine.Types.Timing.MicroSeconds(0),
callFrame: {
functionName,
scriptId: '' as Protocol.Runtime.ScriptId,
url: 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: TraceEngine.Types.TraceEvents.SyntheticTraceEntry[]):
TraceEngine.Handlers.ModelHandlers.Renderer.RendererHandlerData {
const {tree, entryToNode} = TraceEngine.Helpers.TreeHelpers.treify(entries, {filter: {has: () => true}});
const mockThread: TraceEngine.Handlers.ModelHandlers.Renderer.RendererThread = {
tree,
name: 'thread',
entries,
};
const mockProcess: TraceEngine.Handlers.ModelHandlers.Renderer.RendererProcess = {
url: 'url',
isOnMainFrame: true,
threads: new Map([[1 as TraceEngine.Types.TraceEvents.ThreadID, mockThread]]),
};
const renderereEvents: TraceEngine.Types.TraceEvents.TraceEventRendererEvent[] = [];
for (const entry of entries) {
if (TraceEngine.Types.TraceEvents.isTraceEventRendererEvent(entry)) {
renderereEvents.push(entry);
}
}
return {
processes: new Map([[1 as TraceEngine.Types.TraceEvents.ProcessID, mockProcess]]),
compositorTileWorkers: new Map(),
entryToNode,
allTraceEntries: renderereEvents,
};
}
/**
* Mocks an object compatible with the return type of the
* SamplesHandler using only an array of ordered profile calls.
*/
export function makeMockSamplesHandlerData(profileCalls: TraceEngine.Types.TraceEvents.SyntheticProfileCall[]):
TraceEngine.Handlers.ModelHandlers.Samples.SamplesHandlerData {
const {tree, entryToNode} = TraceEngine.Helpers.TreeHelpers.treify(profileCalls, {filter: {has: () => true}});
const profile: Protocol.Profiler.Profile = {
nodes: [],
startTime: profileCalls.at(0)?.ts || TraceEngine.Types.Timing.MicroSeconds(0),
endTime: profileCalls.at(-1)?.ts || TraceEngine.Types.Timing.MicroSeconds(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: TraceEngine.Types.TraceEvents.ProfileID('fake-profile-id'),
};
const profilesInThread = new Map([[1 as TraceEngine.Types.TraceEvents.ThreadID, profileData]]);
return {
profilesInProcess: new Map([[1 as TraceEngine.Types.TraceEvents.ProcessID, profilesInThread]]),
entryToNode,
};
}
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;
}
prepareHighlightedEntryInfo(_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 function getMainThread(data: TraceEngine.Handlers.ModelHandlers.Renderer.RendererHandlerData):
TraceEngine.Handlers.ModelHandlers.Renderer.RendererThread {
let mainThread: TraceEngine.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 TraceParseData = TraceEngine.Handlers.Types.TraceParseData;
export function getBaseTraceParseModelData(overrides: Partial<TraceParseData> = {}): TraceParseData {
return {
Animations: [],
LayoutShifts: {
clusters: [],
sessionMaxScore: 0,
clsWindowID: 0,
prePaintEvents: [],
layoutInvalidationEvents: [],
styleRecalcInvalidationEvents: [],
backendNodeIds: [],
scoreRecords: [],
},
Meta: {
traceBounds: {
min: TraceEngine.Types.Timing.MicroSeconds(0),
max: TraceEngine.Types.Timing.MicroSeconds(100),
range: TraceEngine.Types.Timing.MicroSeconds(100),
},
browserProcessId: TraceEngine.Types.TraceEvents.ProcessID(-1),
browserThreadId: TraceEngine.Types.TraceEvents.ThreadID(-1),
gpuProcessId: TraceEngine.Types.TraceEvents.ProcessID(-1),
gpuThreadId: TraceEngine.Types.TraceEvents.ThreadID(-1),
threadsInProcess: new Map(),
navigationsByFrameId: new Map(),
navigationsByNavigationId: new Map(),
mainFrameId: '',
mainFrameURL: '',
rendererProcessesByFrame: new Map(),
topLevelRendererIds: new Set(),
frameByProcessId: new Map(),
mainFrameNavigations: [],
},
Renderer: {
processes: new Map(),
compositorTileWorkers: new Map(),
entryToNode: new Map(),
allTraceEntries: [],
},
Screenshots: [],
Samples: {
profiles: new Map(),
processes: new Map(),
},
PageLoadMetrics: {metricScoresByFrameId: new Map(), lcpEventNodeIdToDOMNodeMap: new Map()},
UserInteractions: {allEvents: [], interactionEvents: []},
NetworkRequests: {
byOrigin: new Map(),
byTime: [],
},
GPU: {
mainGPUThreadTasks: [],
errorsByUseCase: new Map(),
},
UserTimings: {
timings: [],
},
LargestImagePaint: new Map(),
LargestTextPaint: new Map(),
...overrides,
} as Partial<TraceParseData>as TraceParseData;
}
/**
* 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 TraceEngine.Types.TraceEvents.TraceEventData>(
events: TraceEngine.Types.TraceEvents.TraceEventData[],
predicate: (e: TraceEngine.Types.TraceEvents.TraceEventData) => 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};
}