| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import * as Common from '../../../core/common/common.js'; |
| import * as Platform from '../../../core/platform/platform.js'; |
| import type * as Protocol from '../../../generated/protocol.js'; |
| import type * as CPUProfile from '../../cpu_profile/cpu_profile.js'; |
| import * as Types from '../types/types.js'; |
| |
| import {SyntheticEventsManager} from './SyntheticEvents.js'; |
| import {eventTimingsMicroSeconds} from './Timing.js'; |
| |
| interface MatchingPairableAsyncEvents { |
| syntheticId: string; |
| begin: Types.Events.PairableAsyncBegin|null; |
| end: Types.Events.PairableAsyncEnd|null; |
| instant?: Types.Events.PairableAsyncInstant[]; |
| } |
| |
| /** |
| * Extracts the raw stack trace in known trace events. Most likely than |
| * not you want to use `getZeroIndexedStackTraceForEvent`, which returns |
| * the stack with zero based numbering. Since some trace events are |
| * one based this function can yield unexpected results when used |
| * indiscriminately. |
| * |
| * Note: this only returns the stack trace contained in the payload of |
| * an event, which only contains the synchronous portion of the call |
| * stack. If you want to obtain the whole stack trace you might need to |
| * use the @see Trace.Extras.StackTraceForEvent util. |
| */ |
| export function stackTraceInEvent(event: Types.Events.Event): Types.Events.CallFrame[]|null { |
| if (event.args?.data?.stackTrace) { |
| return event.args.data.stackTrace; |
| } |
| if (event.args?.stackTrace) { |
| return event.args.stackTrace; |
| } |
| if (Types.Events.isRecalcStyle(event)) { |
| return event.args.beginData?.stackTrace || null; |
| } |
| if (Types.Events.isLayout(event)) { |
| return event.args.beginData.stackTrace ?? null; |
| } |
| if (Types.Events.isFunctionCall(event)) { |
| const data = event.args.data; |
| if (!data) { |
| return null; |
| } |
| const {columnNumber, lineNumber, url, scriptId, functionName} = data; |
| if (lineNumber === undefined || functionName === undefined || columnNumber === undefined || |
| scriptId === undefined || url === undefined) { |
| return null; |
| } |
| return [{columnNumber, lineNumber, url, scriptId, functionName}]; |
| } |
| if (Types.Events.isProfileCall(event)) { |
| // Of type Protocol.Runtime.CallFrame, handle accordingly. |
| const callFrame = event.callFrame; |
| if (!callFrame) { |
| return null; |
| } |
| const {columnNumber, lineNumber, url, scriptId, functionName} = callFrame; |
| if (lineNumber === undefined || functionName === undefined || columnNumber === undefined || |
| scriptId === undefined || url === undefined) { |
| return null; |
| } |
| return [{columnNumber, lineNumber, url, scriptId, functionName}]; |
| } |
| return null; |
| } |
| |
| export function extractOriginFromTrace(firstNavigationURL: string): string|null { |
| const url = Common.ParsedURL.ParsedURL.fromString(firstNavigationURL); |
| if (url) { |
| // We do this to save some space in the toolbar - seeing the `www` is less |
| // useful than seeing `foo.com` if it's truncated at narrow widths |
| if (url.host.startsWith('www.')) { |
| return url.host.slice(4); |
| } |
| return url.host; |
| } |
| return null; |
| } |
| |
| export type EventsInThread<T extends Types.Events.Event> = Map<Types.Events.ThreadID, T[]>; |
| /** |
| * Each thread contains events. Events indicate the thread and process IDs, which are |
| * used to store the event in the correct process thread entry below. |
| **/ |
| export function addEventToProcessThread<T extends Types.Events.Event>( |
| event: T, |
| eventsInProcessThread: Map<Types.Events.ProcessID, EventsInThread<T>>, |
| ): void { |
| const {tid, pid} = event; |
| let eventsInThread = eventsInProcessThread.get(pid); |
| if (!eventsInThread) { |
| eventsInThread = new Map<Types.Events.ThreadID, T[]>(); |
| } |
| |
| let events = eventsInThread.get(tid); |
| if (!events) { |
| events = []; |
| } |
| |
| events.push(event); |
| eventsInThread.set(event.tid, events); |
| eventsInProcessThread.set(event.pid, eventsInThread); |
| } |
| |
| export function compareBeginAndEnd(aBeginTime: number, bBeginTime: number, aEndTime: number, bEndTime: number): -1|0|1 { |
| if (aBeginTime < bBeginTime) { |
| return -1; |
| } |
| if (aBeginTime > bBeginTime) { |
| return 1; |
| } |
| |
| if (aEndTime > bEndTime) { |
| return -1; |
| } |
| if (aEndTime < bEndTime) { |
| return 1; |
| } |
| |
| return 0; |
| } |
| |
| export function eventTimeComparator(a: Types.Events.Event, b: Types.Events.Event): -1|0|1 { |
| const aBeginTime = a.ts; |
| const bBeginTime = b.ts; |
| const aDuration = a.dur ?? 0; |
| const bDuration = b.dur ?? 0; |
| const aEndTime = aBeginTime + aDuration; |
| const bEndTime = bBeginTime + bDuration; |
| const timeDifference = compareBeginAndEnd(aBeginTime, bBeginTime, aEndTime, bEndTime); |
| if (timeDifference) { |
| return timeDifference; |
| } |
| |
| // If times are equal, prioritize profile calls over trace events, |
| // since an exactly equal timestamp with a trace event is likely |
| // indicates that the SamplesIntegrator meant to parent the trace |
| // event with the profile call. |
| if (Types.Events.isProfileCall(a) && !Types.Events.isProfileCall(b)) { |
| return -1; |
| } |
| if (Types.Events.isProfileCall(b) && !Types.Events.isProfileCall(a)) { |
| return 1; |
| } |
| return 0; |
| } |
| /** |
| * Sorts all the events in place, in order, by their start time. If they have |
| * the same start time, orders them by longest first. |
| */ |
| export function sortTraceEventsInPlace(events: Types.Events.Event[]): void { |
| events.sort(eventTimeComparator); |
| } |
| |
| /** |
| * Returns an array of ordered events that results after merging the two |
| * ordered input arrays. |
| */ |
| export function mergeEventsInOrder<T1 extends Types.Events.Event, T2 extends Types.Events.Event>( |
| eventsArray1: readonly T1[], eventsArray2: readonly T2[]): Array<T1|T2> { |
| const result = []; |
| let i = 0; |
| let j = 0; |
| while (i < eventsArray1.length && j < eventsArray2.length) { |
| const event1 = eventsArray1[i]; |
| const event2 = eventsArray2[j]; |
| const compareValue = eventTimeComparator(event1, event2); |
| if (compareValue <= 0) { |
| result.push(event1); |
| i++; |
| } |
| if (compareValue === 1) { |
| result.push(event2); |
| j++; |
| } |
| } |
| while (i < eventsArray1.length) { |
| result.push(eventsArray1[i++]); |
| } |
| while (j < eventsArray2.length) { |
| result.push(eventsArray2[j++]); |
| } |
| return result; |
| } |
| |
| export function parseDevtoolsDetails(timingDetail: string, key: string): Types.Extensions.DevToolsObj| |
| Types.Extensions.ExtensionTrackEntryPayloadDeeplink|null { |
| try { |
| // Attempt to parse the detail as an object that might be coming from a |
| // DevTools Perf extension. |
| // Wrapped in a try-catch because timingDetail might either: |
| // 1. Not be `json.parse`-able (it should, but just in case...) |
| // 2. Not be an object - in which case the `in` check will error. |
| // If we hit either of these cases, we just ignore this mark and move on. |
| const detailObj = JSON.parse(timingDetail); |
| if (!(key in detailObj)) { |
| return null; |
| } |
| if (!Types.Extensions.isValidExtensionPayload(detailObj[key])) { |
| return null; |
| } |
| return detailObj[key]; |
| } catch { |
| // No need to worry about this error, just discard this event and don't |
| // treat it as having any useful information for the purposes of extensions. |
| return null; |
| } |
| } |
| |
| export function getNavigationForTraceEvent( |
| event: Types.Events.Event, |
| eventFrameId: string, |
| navigationsByFrameId: Map<string, Types.Events.NavigationStart[]>, |
| ): Types.Events.NavigationStart|null { |
| const navigations = navigationsByFrameId.get(eventFrameId); |
| if (!navigations || eventFrameId === '') { |
| // This event's navigation has been filtered out by the meta handler as a noise event |
| // or contains an empty frameId. |
| return null; |
| } |
| |
| const eventNavigationIndex = |
| Platform.ArrayUtilities.nearestIndexFromEnd(navigations, navigation => navigation.ts <= event.ts); |
| |
| if (eventNavigationIndex === null) { |
| // This event's navigation has been filtered out by the meta handler as a noise event. |
| return null; |
| } |
| return navigations[eventNavigationIndex]; |
| } |
| |
| export function extractId( |
| event: Types.Events.PairableAsync|Types.Events.SyntheticEventPair<Types.Events.PairableAsync>): string|undefined { |
| return event.id ?? event.id2?.global ?? event.id2?.local; |
| } |
| |
| export function activeURLForFrameAtTime( |
| frameId: string, time: Types.Timing.Micro, |
| rendererProcessesByFrame: Map< |
| string, |
| Map<Types.Events.ProcessID, Array<{frame: Types.Events.TraceFrame, window: Types.Timing.TraceWindowMicro}>>>): |
| string|null { |
| const processData = rendererProcessesByFrame.get(frameId); |
| if (!processData) { |
| return null; |
| } |
| for (const processes of processData.values()) { |
| for (const processInfo of processes) { |
| if (processInfo.window.min > time || processInfo.window.max < time) { |
| continue; |
| } |
| return processInfo.frame.url; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * @param node the node attached to the profile call. Here a node represents a function in the call tree. |
| * @param profileId the profile ID that the sample came from that backs this call. |
| * @param sampleIndex the index of the sample in the given profile that this call was created from |
| * @param ts the timestamp of the profile call |
| * @param pid the process ID of the profile call |
| * @param tid the thread ID of the profile call |
| * |
| * See `panels/timeline/docs/profile_calls.md` for more context on how these events are created. |
| */ |
| export function makeProfileCall( |
| node: CPUProfile.ProfileTreeModel.ProfileNode, profileId: Types.Events.ProfileID, sampleIndex: number, |
| ts: Types.Timing.Micro, pid: Types.Events.ProcessID, |
| tid: Types.Events.ThreadID): Types.Events.SyntheticProfileCall { |
| return { |
| cat: '', |
| name: 'ProfileCall', |
| nodeId: node.id, |
| args: {}, |
| ph: Types.Events.Phase.COMPLETE, |
| pid, |
| tid, |
| ts, |
| dur: Types.Timing.Micro(0), |
| callFrame: node.callFrame, |
| sampleIndex, |
| profileId, |
| }; |
| } |
| |
| /** |
| * Matches beginning events with PairableAsyncEnd and PairableAsyncInstant |
| * if provided. Traces may contain multiple instant events so we need to |
| * account for that. Additionally we have seen cases where we might only have a |
| * begin event & instant event(s), with no end event. So we account for that |
| * situation also. |
| * |
| * You might also like to read the models/trace/README.md which has some |
| * documentation on trace IDs. This is important as Perfetto will reuse trace |
| * IDs when emitting events (if they do not overlap). This means it's not as |
| * simple as grouping events by IDs. Instead, we group begin & instant events |
| * by ID as we find them. When we find end events, we then pop any matching |
| * begin/instant events off the stack and group those. That way, if we meet the |
| * same ID later on it doesn't cause us collisions. |
| * |
| * @returns An array of all the matched event groups, along with their ID. Note |
| * that two event groups can have the same ID if they were non-overlapping |
| * events. You cannot rely on ID being unique across a trace. The returned set |
| * of groups are NOT SORTED in any order. |
| */ |
| function matchEvents(unpairedEvents: Types.Events.PairableAsync[]): MatchingPairableAsyncEvents[] { |
| sortTraceEventsInPlace(unpairedEvents); |
| // map to store begin and end of the event |
| const matches: MatchingPairableAsyncEvents[] = []; |
| |
| const beginEventsById = new Map<string, Types.Events.PairableAsyncBegin[]>(); |
| const instantEventsById = new Map<string, Types.Events.PairableAsyncInstant[]>(); |
| for (const event of unpairedEvents) { |
| const id = getSyntheticId(event); |
| if (id === undefined) { |
| continue; |
| } |
| if (Types.Events.isPairableAsyncBegin(event)) { |
| const existingEvents = beginEventsById.get(id) ?? []; |
| existingEvents.push(event); |
| beginEventsById.set(id, existingEvents); |
| } else if (Types.Events.isPairableAsyncInstant(event)) { |
| const existingEvents = instantEventsById.get(id) ?? []; |
| existingEvents.push(event); |
| instantEventsById.set(id, existingEvents); |
| } else if (Types.Events.isPairableAsyncEnd(event)) { |
| // Find matching begin event by ID |
| const beginEventsWithMatchingId = beginEventsById.get(id) ?? []; |
| const beginEvent = beginEventsWithMatchingId.pop(); |
| if (!beginEvent) { |
| continue; |
| } |
| const instantEventsWithMatchingId = instantEventsById.get(id) ?? []; |
| // Find all instant events after the begin event ts. |
| const instantEventsForThisGroup: Types.Events.PairableAsyncInstant[] = []; |
| while (instantEventsWithMatchingId.length > 0) { |
| if (instantEventsWithMatchingId[0].ts >= beginEvent.ts) { |
| const event = instantEventsWithMatchingId.pop(); |
| if (event) { |
| instantEventsForThisGroup.push(event); |
| } |
| } else { |
| break; |
| } |
| } |
| const matchingGroup: MatchingPairableAsyncEvents = { |
| begin: beginEvent, |
| end: event, |
| instant: instantEventsForThisGroup, |
| syntheticId: id, |
| }; |
| matches.push(matchingGroup); |
| } |
| } |
| |
| // At this point we know we have paired up all the Begin & End & Instant |
| // events. But it is possible to see only begin & instant events with the |
| // same ID, and no end event. So now we do a second pass through our begin |
| // events to find any that did not have an end event. If we find some |
| // instant events for the begin event, we create a new group. |
| // Also, because there were no end events, we know that the IDs will be |
| // unique now; e.g. each key in the map should have no more than one item in |
| // it. |
| for (const [id, beginEvents] of beginEventsById) { |
| const beginEvent = beginEvents.pop(); |
| if (!beginEvent) { |
| continue; |
| } |
| const matchingInstantEvents = instantEventsById.get(id); |
| if (matchingInstantEvents?.length) { |
| matches.push({ |
| syntheticId: id, |
| begin: beginEvent, |
| end: null, |
| instant: matchingInstantEvents, |
| }); |
| } |
| } |
| |
| return matches; |
| } |
| |
| export function getSyntheticId(event: Types.Events.PairableAsync): string|undefined { |
| const id = extractId(event); |
| return id && `${event.cat}:${id}:${event.name}`; |
| } |
| |
| function createSortedSyntheticEvents<T extends Types.Events.PairableAsync>( |
| matchedPairs: MatchingPairableAsyncEvents[], |
| ): Array<Types.Events.SyntheticEventPair<T>> { |
| const syntheticEvents: Array<Types.Events.SyntheticEventPair<T>> = []; |
| for (const eventsTriplet of matchedPairs) { |
| const id = eventsTriplet.syntheticId; |
| const beginEvent = eventsTriplet.begin; |
| const endEvent = eventsTriplet.end; |
| const instantEvents = eventsTriplet.instant; |
| if (!beginEvent || !(endEvent || instantEvents)) { |
| // This should never happen, the backend only creates the events once it |
| // has them both (beginEvent & endEvent/instantEvents), so we should never get into this state. |
| // If we do, something is very wrong, so let's just drop that problematic event. |
| continue; |
| } |
| const triplet = {beginEvent, endEvent, instantEvents}; |
| /** |
| * When trying to pair events with instant events present, there are times when these |
| * ASYNC_NESTABLE_INSTANT ('n') don't have a corresponding ASYNC_NESTABLE_END ('e') event. |
| * In these cases, pair without needing the endEvent. |
| */ |
| function eventsArePairable(data: { |
| beginEvent: Types.Events.PairableAsyncBegin, |
| endEvent: Types.Events.PairableAsyncEnd|null, |
| instantEvents?: Types.Events.PairableAsyncInstant[], |
| }): data is Types.Events.SyntheticEventPair<T>['args']['data'] { |
| const instantEventsMatch = data.instantEvents ? data.instantEvents.some(e => id === getSyntheticId(e)) : false; |
| const endEventMatch = data.endEvent ? id === getSyntheticId(data.endEvent) : false; |
| return Boolean(id) && (instantEventsMatch || endEventMatch); |
| } |
| if (!eventsArePairable(triplet)) { |
| continue; |
| } |
| const targetEvent = endEvent || beginEvent; |
| |
| const event = SyntheticEventsManager.registerSyntheticEvent<Types.Events.SyntheticEventPair<T>>({ |
| rawSourceEvent: triplet.beginEvent, |
| cat: targetEvent.cat, |
| ph: targetEvent.ph, |
| pid: targetEvent.pid, |
| tid: targetEvent.tid, |
| id, |
| // Both events have the same name, so it doesn't matter which we pick to |
| // use as the description |
| name: beginEvent.name, |
| dur: Types.Timing.Micro(targetEvent.ts - beginEvent.ts), |
| ts: beginEvent.ts, |
| args: { |
| data: triplet, |
| }, |
| }); |
| |
| if (event.dur < 0) { |
| // We have seen in the backend that sometimes animation events get |
| // generated with multiple begin entries, or multiple end entries, and this |
| // can cause invalid data on the performance panel, so we drop them. |
| // crbug.com/1472375 |
| continue; |
| } |
| syntheticEvents.push(event); |
| } |
| sortTraceEventsInPlace(syntheticEvents); |
| return syntheticEvents; |
| } |
| |
| /** |
| * Groups up sets of async events into synthetic events. |
| * @param unpairedAsyncEvents the raw array of begin, end and async instant |
| * events. These MUST be sorted in timestamp ASC order. |
| */ |
| export function createMatchedSortedSyntheticEvents<T extends Types.Events.PairableAsync>(unpairedAsyncEvents: T[]): |
| Array<Types.Events.SyntheticEventPair<T>> { |
| const matchedPairs = matchEvents(unpairedAsyncEvents); |
| const syntheticEvents = createSortedSyntheticEvents<T>(matchedPairs); |
| return syntheticEvents; |
| } |
| |
| /** |
| * Different trace events return line/column numbers that are 1 or 0 indexed. |
| * This function knows which events return 1 indexed numbers and normalizes |
| * them. The UI expects 0 indexed line numbers, so that is what we return. |
| */ |
| export function getZeroIndexedLineAndColumnForEvent(event: Types.Events.Event): { |
| lineNumber?: number, |
| columnNumber?: number, |
| } { |
| // Some events emit line numbers that are 1 indexed, but the UI layer expects |
| // numbers to be 0 indexed. So here, if the event matches a known 1-indexed |
| // number event, we subtract one from the line and column numbers. |
| // Otherwise, if the event has args.data.lineNumber/colNumber, we return it |
| // as is. |
| const numbers = getRawLineAndColumnNumbersForEvent(event); |
| const {lineNumber, columnNumber} = numbers; |
| |
| switch (event.name) { |
| // All these events have line/column numbers which are 1 indexed; so we |
| // subtract to make them 0 indexed. |
| case Types.Events.Name.FUNCTION_CALL: |
| case Types.Events.Name.EVALUATE_SCRIPT: |
| case Types.Events.Name.COMPILE: |
| case Types.Events.Name.CACHE_SCRIPT: { |
| return { |
| lineNumber: typeof lineNumber === 'number' ? lineNumber - 1 : undefined, |
| columnNumber: typeof columnNumber === 'number' ? columnNumber - 1 : undefined, |
| }; |
| } |
| case Types.Events.Name.PROFILE_CALL: { |
| const callFrame = (event as Types.Events.SyntheticProfileCall).callFrame; |
| return { |
| lineNumber: typeof lineNumber === 'number' ? callFrame.lineNumber - 1 : undefined, |
| columnNumber: typeof columnNumber === 'number' ? callFrame.columnNumber - 1 : undefined, |
| }; |
| } |
| default: { |
| return numbers; |
| } |
| } |
| } |
| |
| /** |
| * Different trace events contain stack traces with line/column numbers |
| * that are 1 or 0 indexed. |
| * This function knows which events return 1 indexed numbers and normalizes |
| * them. The UI expects 0 indexed line numbers, so that is what we return. |
| * |
| * Note: this only returns the stack trace contained in the payload of |
| * an event, which only contains the synchronous portion of the call |
| * stack. If you want to obtain the whole stack trace you might need to |
| * use the @see Trace.Extras.StackTraceForEvent util. |
| */ |
| export function getZeroIndexedStackTraceInEventPayload(event: Types.Events.Event): Types.Events.CallFrame[]|null { |
| const stack = stackTraceInEvent(event); |
| if (!stack) { |
| return null; |
| } |
| |
| switch (event.name) { |
| case Types.Events.Name.SCHEDULE_STYLE_RECALCULATION: |
| case Types.Events.Name.INVALIDATE_LAYOUT: |
| case Types.Events.Name.FUNCTION_CALL: |
| case Types.Events.Name.LAYOUT: |
| case Types.Events.Name.RECALC_STYLE: { |
| return stack.map(makeZeroBasedCallFrame); |
| } |
| |
| default: { |
| if (Types.Events.isUserTiming(event) || Types.Extensions.isSyntheticExtensionEntry(event)) { |
| return stack.map(makeZeroBasedCallFrame); |
| } |
| |
| return stack; |
| } |
| } |
| } |
| |
| /** |
| * Same as getZeroIndexedStackTraceInEventPayload, but only returns the top call frame. |
| */ |
| export function getStackTraceTopCallFrameInEventPayload(event: Types.Events.Event): Types.Events.CallFrame|null { |
| const stack = stackTraceInEvent(event); |
| if (!stack || stack.length === 0) { |
| return null; |
| } |
| |
| switch (event.name) { |
| case Types.Events.Name.SCHEDULE_STYLE_RECALCULATION: |
| case Types.Events.Name.INVALIDATE_LAYOUT: |
| case Types.Events.Name.FUNCTION_CALL: |
| case Types.Events.Name.LAYOUT: |
| case Types.Events.Name.RECALC_STYLE: { |
| return makeZeroBasedCallFrame(stack[0]); |
| } |
| |
| default: { |
| if (Types.Events.isUserTiming(event) || Types.Extensions.isSyntheticExtensionEntry(event)) { |
| return makeZeroBasedCallFrame(stack[0]); |
| } |
| |
| return stack[0]; |
| } |
| } |
| } |
| |
| export function rawCallFrameForEntry(entry: Types.Events.Event): Protocol.Runtime.CallFrame|null { |
| if (Types.Events.isProfileCall(entry)) { |
| return entry.callFrame; |
| } |
| |
| const topCallFrame = getStackTraceTopCallFrameInEventPayload(entry); |
| if (topCallFrame) { |
| return topCallFrame as Protocol.Runtime.CallFrame; |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Given a 1-based call frame creates a 0-based one. |
| */ |
| export function makeZeroBasedCallFrame(callFrame: Types.Events.CallFrame): Types.Events.CallFrame { |
| const normalizedCallFrame = {...callFrame}; |
| |
| normalizedCallFrame.lineNumber = callFrame.lineNumber && callFrame.lineNumber - 1; |
| normalizedCallFrame.columnNumber = callFrame.columnNumber && callFrame.columnNumber - 1; |
| return normalizedCallFrame; |
| } |
| |
| /** |
| * NOTE: you probably do not want this function! (Which is why it is not exported). |
| * |
| * Some trace events have 0 indexed line/column numbers, and others have 1 |
| * indexed. This function does NOT normalize them, but |
| * `getZeroIndexedLineAndColumnNumbersForEvent` does. It is best to use that! |
| * |
| * @see {@link getZeroIndexedLineAndColumnForEvent} |
| **/ |
| function getRawLineAndColumnNumbersForEvent(event: Types.Events.Event): { |
| lineNumber?: number, |
| columnNumber?: number, |
| } { |
| if (!event.args?.data) { |
| return { |
| lineNumber: undefined, |
| columnNumber: undefined, |
| }; |
| } |
| let lineNumber: number|undefined = undefined; |
| let columnNumber: number|undefined = undefined; |
| if ('lineNumber' in event.args.data && typeof event.args.data.lineNumber === 'number') { |
| lineNumber = event.args.data.lineNumber; |
| } |
| if ('columnNumber' in event.args.data && typeof event.args.data.columnNumber === 'number') { |
| columnNumber = event.args.data.columnNumber; |
| } |
| |
| return {lineNumber, columnNumber}; |
| } |
| |
| export function frameIDForEvent(event: Types.Events.Event): string|null { |
| // There are a few events (for example RecalcStyle, ParseHTML) that have |
| // the frame stored in args.beginData |
| // Rather than list them all we just check for the presence of the field, so |
| // we are robust against future trace events also doing this. |
| // This check seems very robust, but it also helps satisfy TypeScript and |
| // prevents us against unexpected data. |
| if (event.args && 'beginData' in event.args && typeof event.args.beginData === 'object' && |
| event.args.beginData !== null && 'frame' in event.args.beginData && |
| typeof event.args.beginData.frame === 'string') { |
| return event.args.beginData.frame; |
| } |
| // Otherwise, we expect frame to be in args.data |
| if (event.args?.data?.frame) { |
| return event.args.data.frame; |
| } |
| |
| // No known frame for this event. |
| return null; |
| } |
| |
| const DevToolsTimelineEventCategory = 'disabled-by-default-devtools.timeline'; |
| export function isTopLevelEvent(event: Types.Events.Event): boolean { |
| return event.cat.includes(DevToolsTimelineEventCategory) && event.name === Types.Events.Name.RUN_TASK; |
| } |
| |
| export function isExtensionUrl(url: string): boolean { |
| return url.startsWith('extensions:') || url.startsWith('chrome-extension:'); |
| } |
| |
| function topLevelEventIndexEndingAfter(events: Types.Events.Event[], time: Types.Timing.Micro): number { |
| let index = Platform.ArrayUtilities.upperBound(events, time, (time, event) => time - event.ts) - 1; |
| while (index > 0 && !isTopLevelEvent(events[index])) { |
| index--; |
| } |
| return Math.max(index, 0); |
| } |
| export function findRecalcStyleEvents( |
| events: Types.Events.Event[], startTime: Types.Timing.Micro, |
| endTime?: Types.Timing.Micro): Types.Events.RecalcStyle[] { |
| const foundEvents: Types.Events.RecalcStyle[] = []; |
| const startEventIndex = topLevelEventIndexEndingAfter(events, startTime); |
| for (let i = startEventIndex; i < events.length; i++) { |
| const event = events[i]; |
| if (!Types.Events.isRecalcStyle(event)) { |
| continue; |
| } |
| if (event.ts >= (endTime || Infinity)) { |
| continue; |
| } |
| foundEvents.push(event); |
| } |
| return foundEvents; |
| } |
| |
| export function findNextEventAfterTimestamp<T extends Types.Events.Event>(candidates: T[], ts: Types.Timing.Micro): T| |
| null { |
| const index = Platform.ArrayUtilities.nearestIndexFromBeginning(candidates, candidate => ts < candidate.ts); |
| return index === null ? null : candidates[index]; |
| } |
| |
| export function findPreviousEventBeforeTimestamp<T extends Types.Events.Event>( |
| candidates: T[], ts: Types.Timing.Micro): T|null { |
| const index = Platform.ArrayUtilities.nearestIndexFromEnd(candidates, candidate => candidate.ts < ts); |
| return index === null ? null : candidates[index]; |
| } |
| |
| export interface ForEachEventConfig { |
| onStartEvent: (event: Types.Events.Event) => void; |
| onEndEvent: (event: Types.Events.Event) => void; |
| onInstantEvent?: (event: Types.Events.Event) => void; |
| eventFilter?: (event: Types.Events.Event) => boolean; |
| startTime?: Types.Timing.Micro; |
| endTime?: Types.Timing.Micro; |
| /* If async events should be skipped. Defaults to true */ |
| ignoreAsyncEvents?: boolean; |
| } |
| |
| /** |
| * Iterates events in a tree hierarchically, from top to bottom, |
| * calling back on every event's start and end in the order |
| * dictated by the corresponding timestamp. |
| * |
| * Events are assumed to be in ascendent order by timestamp. |
| * |
| * Events with 0 duration are treated as instant events. These do not have a |
| * begin and end, but will be passed to the config.onInstantEvent callback as |
| * they are discovered. Do not provide this callback if you are not interested |
| * in them. |
| * |
| * For example, given this tree, the following callbacks |
| * are expected to be made in the following order |
| * |---------------A---------------| |
| * |------B------||-------D------| |
| * |---C---| |
| * |
| * 1. Start A |
| * 3. Start B |
| * 4. Start C |
| * 5. End C |
| * 6. End B |
| * 7. Start D |
| * 8. End D |
| * 9. End A |
| * |
| * By default, async events are skipped. This behaviour can be |
| * overridden making use of the config.ignoreAsyncEvents parameter. |
| */ |
| export function forEachEvent( |
| events: Types.Events.Event[], |
| config: ForEachEventConfig, |
| ): void { |
| const globalStartTime = config.startTime ?? Types.Timing.Micro(0); |
| const globalEndTime = config.endTime || Types.Timing.Micro(Infinity); |
| const ignoreAsyncEvents = config.ignoreAsyncEvents === false ? false : true; |
| |
| const stack: Types.Events.Event[] = []; |
| const startEventIndex = topLevelEventIndexEndingAfter(events, globalStartTime); |
| for (let i = startEventIndex; i < events.length; i++) { |
| const currentEvent = events[i]; |
| const currentEventTimings = eventTimingsMicroSeconds(currentEvent); |
| if (currentEventTimings.endTime < globalStartTime) { |
| continue; |
| } |
| if (currentEventTimings.startTime > globalEndTime) { |
| break; |
| } |
| |
| const isIgnoredAsyncEvent = ignoreAsyncEvents && Types.Events.isPhaseAsync(currentEvent.ph); |
| if (isIgnoredAsyncEvent || Types.Events.isFlowPhase(currentEvent.ph)) { |
| continue; |
| } |
| |
| // If we have now reached an event that is after a bunch of events, we need |
| // to call the onEndEvent callback for those events before moving on. |
| let lastEventOnStack = stack.at(-1); |
| let lastEventEndTime = lastEventOnStack ? eventTimingsMicroSeconds(lastEventOnStack).endTime : null; |
| while (lastEventOnStack && lastEventEndTime && lastEventEndTime <= currentEventTimings.startTime) { |
| stack.pop(); |
| config.onEndEvent(lastEventOnStack); |
| lastEventOnStack = stack.at(-1); |
| lastEventEndTime = lastEventOnStack ? eventTimingsMicroSeconds(lastEventOnStack).endTime : null; |
| } |
| |
| // Now we have dealt with all events prior to this one, see if we need to care about this one. |
| if (config.eventFilter && !config.eventFilter(currentEvent)) { |
| // The user has chosen to filter this event out, so continue on and do nothing |
| continue; |
| } |
| |
| if (currentEventTimings.duration) { |
| config.onStartEvent(currentEvent); |
| stack.push(currentEvent); |
| } else if (config.onInstantEvent) { |
| // An event with 0 duration is an instant event. |
| config.onInstantEvent(currentEvent); |
| } |
| } |
| |
| // Now we have finished looping over all events; any events remaining on the |
| // stack need to have their onEndEvent called. |
| while (stack.length) { |
| const last = stack.pop(); |
| if (last) { |
| config.onEndEvent(last); |
| } |
| } |
| } |
| |
| // Parsed categories are cached to prevent calling cat.split() |
| // multiple times on the same categories string. |
| const parsedCategories = new Map<string, Set<string>>(); |
| export function eventHasCategory(event: Types.Events.Event, category: string): boolean { |
| let parsedCategoriesForEvent = parsedCategories.get(event.cat); |
| if (!parsedCategoriesForEvent) { |
| parsedCategoriesForEvent = new Set(event.cat.split(',') || []); |
| } |
| return parsedCategoriesForEvent.has(category); |
| } |
| |
| /** |
| * This compares Types.Events.CallFrame with Protocol.Runtime.CallFrame and checks for equality. |
| */ |
| export function isMatchingCallFrame( |
| eventFrame: Types.Events.CallFrame, nodeFrame: Protocol.Runtime.CallFrame): boolean { |
| return eventFrame.columnNumber === nodeFrame.columnNumber && eventFrame.lineNumber === nodeFrame.lineNumber && |
| String(eventFrame.scriptId) === nodeFrame.scriptId && eventFrame.url === nodeFrame.url && |
| eventFrame.functionName === nodeFrame.functionName; |
| } |
| |
| export function eventContainsTimestamp(event: Types.Events.Event, ts: Types.Timing.Micro): boolean { |
| return event.ts <= ts && event.ts + (event.dur || 0) >= ts; |
| } |
| |
| export function extractSampleTraceId(event: Types.Events.Event): number|null { |
| if (!event.args) { |
| return null; |
| } |
| if ('beginData' in event.args) { |
| const beginData = event.args['beginData'] as {sampleTraceId?: number}; |
| return beginData.sampleTraceId ?? null; |
| } |
| return event.args?.sampleTraceId ?? event.args?.data?.sampleTraceId ?? null; |
| } |
| |
| /** |
| * This exactly matches Trace.Styles.visibleTypes. See the runtime verification in maybeInitStylesMap. |
| * TODO(crbug.com/410884528) |
| **/ |
| export const VISIBLE_TRACE_EVENT_TYPES = new Set<Types.Events.Name>([ |
| Types.Events.Name.ABORT_POST_TASK_CALLBACK, |
| Types.Events.Name.ANIMATION, |
| Types.Events.Name.ASYNC_TASK, |
| Types.Events.Name.BACKGROUND_DESERIALIZE, |
| Types.Events.Name.CACHE_MODULE, |
| Types.Events.Name.CACHE_SCRIPT, |
| Types.Events.Name.CANCEL_ANIMATION_FRAME, |
| Types.Events.Name.CANCEL_IDLE_CALLBACK, |
| Types.Events.Name.COMMIT, |
| Types.Events.Name.COMPILE_CODE, |
| Types.Events.Name.COMPILE_MODULE, |
| Types.Events.Name.COMPILE, |
| Types.Events.Name.COMPOSITE_LAYERS, |
| Types.Events.Name.COMPUTE_INTERSECTION, |
| Types.Events.Name.CONSOLE_TIME, |
| Types.Events.Name.CPPGC_SWEEP, |
| Types.Events.Name.CRYPTO_DO_DECRYPT_REPLY, |
| Types.Events.Name.CRYPTO_DO_DECRYPT, |
| Types.Events.Name.CRYPTO_DO_DIGEST_REPLY, |
| Types.Events.Name.CRYPTO_DO_DIGEST, |
| Types.Events.Name.CRYPTO_DO_ENCRYPT_REPLY, |
| Types.Events.Name.CRYPTO_DO_ENCRYPT, |
| Types.Events.Name.CRYPTO_DO_SIGN_REPLY, |
| Types.Events.Name.CRYPTO_DO_SIGN, |
| Types.Events.Name.CRYPTO_DO_VERIFY_REPLY, |
| Types.Events.Name.CRYPTO_DO_VERIFY, |
| Types.Events.Name.DECODE_IMAGE, |
| Types.Events.Name.EMBEDDER_CALLBACK, |
| Types.Events.Name.EVALUATE_MODULE, |
| Types.Events.Name.EVALUATE_SCRIPT, |
| Types.Events.Name.EVENT_DISPATCH, |
| Types.Events.Name.EVENT_TIMING, |
| Types.Events.Name.FINALIZE_DESERIALIZATION, |
| Types.Events.Name.FIRE_ANIMATION_FRAME, |
| Types.Events.Name.FIRE_IDLE_CALLBACK, |
| Types.Events.Name.FUNCTION_CALL, |
| Types.Events.Name.GC_COLLECT_GARBARGE, |
| Types.Events.Name.GC, |
| Types.Events.Name.GPU_TASK, |
| Types.Events.Name.HANDLE_POST_MESSAGE, |
| Types.Events.Name.HIT_TEST, |
| Types.Events.Name.JS_SAMPLE, |
| Types.Events.Name.LAYERIZE, |
| Types.Events.Name.LAYOUT, |
| Types.Events.Name.MAJOR_GC, |
| Types.Events.Name.MINOR_GC, |
| Types.Events.Name.OPTIMIZE_CODE, |
| Types.Events.Name.PAINT_SETUP, |
| Types.Events.Name.PAINT, |
| Types.Events.Name.PARSE_AUTHOR_STYLE_SHEET, |
| Types.Events.Name.PARSE_HTML, |
| Types.Events.Name.PRE_PAINT, |
| Types.Events.Name.PROFILE_CALL, |
| Types.Events.Name.PROGRAM, |
| Types.Events.Name.RASTER_TASK, |
| Types.Events.Name.REQUEST_ANIMATION_FRAME, |
| Types.Events.Name.REQUEST_IDLE_CALLBACK, |
| Types.Events.Name.RESOURCE_FINISH, |
| Types.Events.Name.RESOURCE_RECEIVE_DATA, |
| Types.Events.Name.RESOURCE_RECEIVE_RESPONSE, |
| Types.Events.Name.RESOURCE_SEND_REQUEST, |
| Types.Events.Name.RESOURCE_WILL_SEND_REQUEST, |
| Types.Events.Name.RUN_MICROTASKS, |
| Types.Events.Name.RUN_POST_TASK_CALLBACK, |
| Types.Events.Name.RUN_TASK, |
| Types.Events.Name.SCHEDULE_POST_MESSAGE, |
| Types.Events.Name.SCHEDULE_POST_TASK_CALLBACK, |
| Types.Events.Name.SCHEDULE_STYLE_RECALCULATION, |
| Types.Events.Name.SCROLL_LAYER, |
| Types.Events.Name.START_PROFILING, |
| Types.Events.Name.STREAMING_COMPILE_SCRIPT_PARSING, |
| Types.Events.Name.STREAMING_COMPILE_SCRIPT_WAITING, |
| Types.Events.Name.STREAMING_COMPILE_SCRIPT, |
| Types.Events.Name.SYNTHETIC_LAYOUT_SHIFT_CLUSTER, |
| Types.Events.Name.SYNTHETIC_LAYOUT_SHIFT, |
| Types.Events.Name.TIME_STAMP, |
| Types.Events.Name.TIMER_FIRE, |
| Types.Events.Name.TIMER_INSTALL, |
| Types.Events.Name.TIMER_REMOVE, |
| Types.Events.Name.UPDATE_LAYER_TREE, |
| Types.Events.Name.RECALC_STYLE, |
| Types.Events.Name.USER_TIMING, |
| Types.Events.Name.V8_CONSOLE_RUN_TASK, |
| Types.Events.Name.WASM_CACHED_MODULE, |
| Types.Events.Name.WASM_COMPILED_MODULE, |
| Types.Events.Name.WASM_MODULE_CACHE_HIT, |
| Types.Events.Name.WASM_MODULE_CACHE_INVALID, |
| Types.Events.Name.WASM_STREAM_FROM_RESPONSE_CALLBACK, |
| Types.Events.Name.WEB_SOCKET_CREATE, |
| Types.Events.Name.WEB_SOCKET_DESTROY, |
| Types.Events.Name.WEB_SOCKET_RECEIVE_HANDSHAKE_REQUEST, |
| Types.Events.Name.WEB_SOCKET_RECEIVE, |
| Types.Events.Name.WEB_SOCKET_SEND_HANDSHAKE_REQUEST, |
| Types.Events.Name.WEB_SOCKET_SEND, |
| Types.Events.Name.XHR_LOAD, |
| Types.Events.Name.XHR_READY_STATE_CHANGED, |
| ]); |