blob: 717e86483dbfee7f5a532d8c2d406696a04df799 [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {assert} from 'chai';
import type * as Protocol from '../generated/protocol.js';
import * as CPUProfile from '../models/cpu_profile/cpu_profile.js';
import * as Trace from '../models/trace/trace.js';
/**
* 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;
assert(tree, `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];
assert(node, `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 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: {},
};
}
/**
* 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),
recalcStyleEvents: entries.filter(Trace.Types.Events.isRecalcStyle),
};
const mockProcess: Trace.Handlers.ModelHandlers.Renderer.RendererProcess = {
url: 'url',
isOnMainFrame: true,
threads: new Map([[tid as Trace.Types.Events.ThreadID, mockThread]]),
};
return {
processes: new Map([[pid as Trace.Types.Events.ProcessID, mockProcess]]),
compositorTileWorkers: new Map(),
entryToNode,
entityMappings: {
entityByEvent: new Map(),
eventsByEntity: new Map(),
createdEntityCache: new Map(),
entityByUrlCache: 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 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;
}
}
}
assert.isNotNull(mainThread, 'Could not find main thread');
return mainThread;
}
export function getBaseTraceHandlerData(overrides: Partial<Trace.Handlers.Types.HandlerData> = {}):
Trace.TraceModel.ParsedTrace {
const data = {
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(),
entityMappings: {
entityByEvent: new Map(),
eventsByEntity: new Map(),
createdEntityCache: new Map(),
entityByUrlCache: 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(),
incompleteInitiator: new Map(),
byTime: [],
webSocket: [],
entityMappings: {
entityByEvent: new Map(),
eventsByEntity: new Map(),
createdEntityCache: new Map(),
entityByUrlCache: 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(),
paintEventToCorrectedDisplaySize: new Map(),
didCorrectForHostDpr: false,
},
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: {
dataForRecalcStyleEvent: new Map(),
invalidatedNodeList: [],
},
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,
} as Trace.Handlers.Types.HandlerData;
return {
data,
insights: null,
traceEvents: [],
metadata: {},
// @ts-expect-error
syntheticEventsManager: null,
};
}
/**
* 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);
assert(match, 'Failed to find matching event of type');
return match;
}
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);
}
/**
* Creates a mock `ParsedTrace` object for use in tests. This is a simple
* cast of an empty object to the `ParsedTrace` type to reduce noise in tests
* that only need a typed trace object without specific data.
*/
export function makeFakeParsedTrace(): Trace.TraceModel.ParsedTrace {
return {} as unknown as Trace.TraceModel.ParsedTrace;
}
export function milliseconds(x: number): Trace.Types.Timing.Milli {
return Trace.Types.Timing.Milli(x);
}
export function getAllNetworkRequestsByHost(networkRequests: Trace.Types.Events.SyntheticNetworkRequest[],
host: string): Trace.Types.Events.SyntheticNetworkRequest[] {
const reqs = networkRequests.filter(r => {
const parsedUrl = new URL(r.args.data.url);
return parsedUrl.host === host;
});
return reqs;
}
const allThreadEntriesForTraceCache = new WeakMap<Trace.TraceModel.ParsedTrace, Trace.Types.Events.Event[]>();
/**
* A function to get a list of all thread entries that exist. This is
* reasonably expensive, so it's cached to avoid a huge impact on our test suite
* speed.
*/
export function allThreadEntriesInTrace(parsedTrace: Trace.TraceModel.ParsedTrace): Trace.Types.Events.Event[] {
const fromCache = allThreadEntriesForTraceCache.get(parsedTrace);
if (fromCache) {
return fromCache;
}
const allEvents: Trace.Types.Events.Event[] = [];
for (const process of parsedTrace.data.Renderer.processes.values()) {
for (const thread of process.threads.values()) {
for (const entry of thread.entries) {
allEvents.push(entry);
}
}
}
Trace.Helpers.Trace.sortTraceEventsInPlace(allEvents);
allThreadEntriesForTraceCache.set(parsedTrace, allEvents);
return allEvents;
}
export interface PerformanceAPIExtensionTestData {
detail: {devtools?: Trace.Types.Extensions.DevToolsObj};
name: string;
start?: string|number;
end?: string|number;
ts: number;
dur?: number;
}
export interface ConsoleAPIExtensionTestData {
name: string;
start?: string|number;
end?: string|number;
track?: string;
trackGroup?: string;
color?: string;
ts: number;
}
let idCounter = 0;
export function makeTimingEventWithPerformanceExtensionData(
{name, ts: tsMicro, detail, dur: durMicro}: PerformanceAPIExtensionTestData): Trace.Types.Events.Event[] {
const isMark = durMicro === undefined;
const currentId = idCounter++;
const traceEventBase = {
cat: 'blink.user_timing',
pid: Trace.Types.Events.ProcessID(2017),
tid: Trace.Types.Events.ThreadID(259),
id2: {local: `${currentId}`},
};
const stringDetail = JSON.stringify(detail);
const args = isMark ? {data: {detail: stringDetail}} : {detail: stringDetail};
const firstEvent = {
args,
name,
ph: isMark ? Trace.Types.Events.Phase.INSTANT : Trace.Types.Events.Phase.ASYNC_NESTABLE_START,
ts: Trace.Types.Timing.Micro(tsMicro),
...traceEventBase,
} as Trace.Types.Events.Event;
if (isMark) {
return [firstEvent];
}
return [
firstEvent,
{
name,
...traceEventBase,
ts: Trace.Types.Timing.Micro(tsMicro + (durMicro || 0)),
ph: Trace.Types.Events.Phase.ASYNC_NESTABLE_END,
},
];
}
export function makeTimingEventWithConsoleExtensionData(
{name, ts, start, end, track, trackGroup, color}: ConsoleAPIExtensionTestData):
Trace.Types.Events.ConsoleTimeStamp {
return {
cat: 'devtools.timeline',
pid: Trace.Types.Events.ProcessID(2017),
tid: Trace.Types.Events.ThreadID(259),
name: Trace.Types.Events.Name.TIME_STAMP,
args: {
data: {
message: name,
start,
end,
track,
trackGroup,
color,
},
},
ts: Trace.Types.Timing.Micro(ts),
ph: Trace.Types.Events.Phase.INSTANT,
};
}
export async function createTraceExtensionDataFromPerformanceAPITestInput(
extensionData: PerformanceAPIExtensionTestData[]):
Promise<Trace.Handlers.ModelHandlers.ExtensionTraceData.ExtensionTraceData> {
const events = extensionData.flatMap(makeTimingEventWithPerformanceExtensionData).sort((e1, e2) => e1.ts - e2.ts);
return await createTraceExtensionDataFromEvents(events);
}
export async function createTraceExtensionDataFromEvents(events: Trace.Types.Events.Event[]):
Promise<Trace.Handlers.ModelHandlers.ExtensionTraceData.ExtensionTraceData> {
Trace.Helpers.SyntheticEvents.SyntheticEventsManager.createAndActivate(events);
Trace.Handlers.ModelHandlers.UserTimings.reset();
for (const event of events) {
Trace.Handlers.ModelHandlers.UserTimings.handleEvent(event);
}
await Trace.Handlers.ModelHandlers.UserTimings.finalize();
Trace.Handlers.ModelHandlers.ExtensionTraceData.reset();
// ExtensionTraceData handler doesn't need to handle events since
// it only consumes the output of the user timings handler.
await Trace.Handlers.ModelHandlers.ExtensionTraceData.finalize();
return Trace.Handlers.ModelHandlers.ExtensionTraceData.data();
}