chrome-devtools-frontend
Version:
Chrome DevTools UI
951 lines (837 loc) • 32.2 kB
text/typescript
// Copyright 2014 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 Common from '../common/common.js';
import {type EventPayload} from './TracingManager.js';
import * as TraceEngine from '../../models/trace/trace.js';
type IgnoreListArgs = {
[key: string]: string|number|ObjectSnapshot,
};
export class TracingModel {
readonly #title: string|undefined;
readonly #processById: Map<string|number, Process>;
readonly #processByName: Map<string, Process>;
#minimumRecordTimeInternal: number;
#maximumRecordTimeInternal: number;
readonly #devToolsMetadataEventsInternal: Event[];
#asyncEvents: AsyncEvent[];
readonly #openAsyncEvents: Map<string, AsyncEvent>;
readonly #openNestableAsyncEvents: Map<string, AsyncEvent[]>;
readonly #profileGroups: Map<string, ProfileEventsGroup>;
readonly #parsedCategories: Map<string, Set<string>>;
readonly #mainFrameNavStartTimes: Map<string, PayloadEvent>;
readonly #allEventsPayload: EventPayload[] = [];
constructor(title?: string) {
this.#title = title;
this.#processById = new Map();
this.#processByName = new Map();
this.#minimumRecordTimeInternal = Number(Infinity);
this.#maximumRecordTimeInternal = Number(-Infinity);
this.#devToolsMetadataEventsInternal = [];
this.#asyncEvents = [];
this.#openAsyncEvents = new Map();
this.#openNestableAsyncEvents = new Map();
this.#profileGroups = new Map();
this.#parsedCategories = new Map();
this.#mainFrameNavStartTimes = new Map();
}
static isTopLevelEvent(event: CompatibleTraceEvent): boolean {
return eventHasCategory(event, DevToolsTimelineEventCategory) && event.name === 'RunTask' ||
eventHasCategory(event, LegacyTopLevelEventCategory) ||
eventHasCategory(event, DevToolsMetadataEventCategory) &&
event.name === 'Program'; // Older timelines may have this instead of toplevel.
}
static extractId(payload: EventPayload): string|undefined {
const scope = payload.scope || '';
if (typeof payload.id2 === 'undefined') {
return scope && payload.id ? `${scope}@${payload.id}` : payload.id;
}
const id2 = payload.id2;
if (typeof id2 === 'object' && ('global' in id2) !== ('local' in id2)) {
return typeof id2['global'] !== 'undefined' ? `:${scope}:${id2['global']}` :
`:${scope}:${payload.pid}:${id2['local']}`;
}
console.error(
`Unexpected id2 field at ${payload.ts / 1000}, one and only one of 'local' and 'global' should be present.`);
return undefined;
}
static browserMainThread(tracingModel: TracingModel): Thread|null {
const processes = tracingModel.sortedProcesses();
// Avoid warning for an empty #model.
if (!processes.length) {
return null;
}
const browserMainThreadName = 'CrBrowserMain';
const browserProcesses = [];
const browserMainThreads = [];
for (const process of processes) {
if (process.name().toLowerCase().endsWith('browser')) {
browserProcesses.push(process);
}
browserMainThreads.push(...process.sortedThreads().filter(t => t.name() === browserMainThreadName));
}
if (browserMainThreads.length === 1) {
return browserMainThreads[0];
}
if (browserProcesses.length === 1) {
return browserProcesses[0].threadByName(browserMainThreadName);
}
const tracingStartedInBrowser =
tracingModel.devToolsMetadataEvents().filter(e => e.name === 'TracingStartedInBrowser');
if (tracingStartedInBrowser.length === 1) {
return tracingStartedInBrowser[0].thread;
}
Common.Console.Console.instance().error(
'Failed to find browser main thread in trace, some timeline features may be unavailable');
return null;
}
allRawEvents(): readonly EventPayload[] {
return this.#allEventsPayload;
}
devToolsMetadataEvents(): Event[] {
return this.#devToolsMetadataEventsInternal;
}
addEvents(events: readonly EventPayload[]): void {
for (let i = 0; i < events.length; ++i) {
this.addEvent(events[i]);
}
}
tracingComplete(): void {
this.processPendingAsyncEvents();
for (const process of this.#processById.values()) {
for (const thread of process.threads.values()) {
thread.tracingComplete();
}
}
}
private addEvent(payload: EventPayload): void {
this.#allEventsPayload.push(payload);
let process = this.#processById.get(payload.pid);
if (!process) {
process = new Process(this, payload.pid);
this.#processById.set(payload.pid, process);
}
const timestamp = payload.ts / 1000;
// We do allow records for unrelated threads to arrive out-of-order,
// so there's a chance we're getting records from the past.
if (timestamp && timestamp < this.#minimumRecordTimeInternal &&
eventPhasesOfInterestForTraceBounds.has(payload.ph as TraceEngine.Types.TraceEvents.Phase) &&
// UMA related events are ignored when calculating the minimumRecordTime because they might
// be related to previous navigations that happened before the current trace started and
// will currently not be displayed anyways.
// See crbug.com/1201198
(!payload.name.endsWith('::UMA'))) {
this.#minimumRecordTimeInternal = timestamp;
}
if (payload.name === 'TracingStartedInBrowser') {
// If we received a timestamp for tracing start, use that for minimumRecordTime.
this.#minimumRecordTimeInternal = timestamp;
}
// Track only main thread navigation start items. This is done by tracking
// isOutermostMainFrame, and whether documentLoaderURL is set.
if (payload.name === 'navigationStart') {
const data = (payload.args.data as {
isLoadingMainFrame: boolean,
documentLoaderURL: string,
navigationId: string,
isOutermostMainFrame?: boolean,
} | null);
if (data) {
const {isLoadingMainFrame, documentLoaderURL, navigationId, isOutermostMainFrame} = data;
if ((isOutermostMainFrame ?? isLoadingMainFrame) && documentLoaderURL !== '') {
const thread = process.threadById(payload.tid);
const navStartEvent = PayloadEvent.fromPayload(payload, thread);
this.#mainFrameNavStartTimes.set(navigationId, navStartEvent);
}
}
}
if (eventPhasesOfInterestForTraceBounds.has(payload.ph as TraceEngine.Types.TraceEvents.Phase)) {
const endTimeStamp = (payload.ts + (payload.dur || 0)) / 1000;
this.#maximumRecordTimeInternal = Math.max(this.#maximumRecordTimeInternal, endTimeStamp);
}
const event = process.addEvent(payload);
if (!event) {
return;
}
if (payload.ph === TraceEngine.Types.TraceEvents.Phase.SAMPLE) {
this.addSampleEvent(event);
return;
}
// Build async event when we've got events from all threads & processes, so we can sort them and process in the
// chronological order. However, also add individual async events to the thread flow (above), so we can easily
// display them on the same chart as other events, should we choose so.
if (TraceEngine.Types.TraceEvents.isAsyncPhase(payload.ph)) {
this.#asyncEvents.push((event as AsyncEvent));
}
if (event.hasCategory(DevToolsMetadataEventCategory)) {
this.#devToolsMetadataEventsInternal.push(event);
}
if (payload.ph !== TraceEngine.Types.TraceEvents.Phase.METADATA) {
return;
}
switch (payload.name) {
case MetadataEvent.ProcessSortIndex: {
process.setSortIndex(payload.args['sort_index']);
break;
}
case MetadataEvent.ProcessName: {
const processName = payload.args['name'];
process.setName(processName);
this.#processByName.set(processName, process);
break;
}
case MetadataEvent.ThreadSortIndex: {
process.threadById(payload.tid).setSortIndex(payload.args['sort_index']);
break;
}
case MetadataEvent.ThreadName: {
process.threadById(payload.tid).setName(payload.args['name']);
break;
}
}
}
private addSampleEvent(event: Event): void {
const id = `${event.thread.process().id()}:${event.id}`;
const group = this.#profileGroups.get(id);
if (group) {
group.addChild(event);
} else {
this.#profileGroups.set(id, new ProfileEventsGroup(event));
}
}
profileGroup(event: Event): ProfileEventsGroup|null {
return this.#profileGroups.get(`${event.thread.process().id()}:${event.id}`) || null;
}
minimumRecordTime(): number {
return this.#minimumRecordTimeInternal;
}
maximumRecordTime(): number {
return this.#maximumRecordTimeInternal;
}
navStartTimes(): Map<string, PayloadEvent> {
return this.#mainFrameNavStartTimes;
}
sortedProcesses(): Process[] {
return NamedObject.sort([...this.#processById.values()]);
}
getProcessByName(name: string): Process|null {
return this.#processByName.get(name) ?? null;
}
getProcessById(pid: number): Process|null {
return this.#processById.get(pid) || null;
}
getThreadByName(processName: string, threadName: string): Thread|null {
const process = this.getProcessByName(processName);
return process && process.threadByName(threadName);
}
private processPendingAsyncEvents(): void {
this.#asyncEvents.sort(Event.compareStartTime);
for (let i = 0; i < this.#asyncEvents.length; ++i) {
const event = this.#asyncEvents[i];
if (TraceEngine.Types.TraceEvents.isNestableAsyncPhase(event.phase)) {
this.addNestableAsyncEvent(event);
} else {
this.addAsyncEvent(event);
}
}
this.#asyncEvents = [];
this.closeOpenAsyncEvents();
}
private closeOpenAsyncEvents(): void {
for (const event of this.#openAsyncEvents.values()) {
event.setEndTime(this.#maximumRecordTimeInternal);
// FIXME: remove this once we figure a better way to convert async console
// events to sync [waterfall] timeline records.
event.steps[0].setEndTime(this.#maximumRecordTimeInternal);
}
this.#openAsyncEvents.clear();
for (const eventStack of this.#openNestableAsyncEvents.values()) {
while (eventStack.length) {
const event = eventStack.pop();
if (!event) {
continue;
}
event.setEndTime(this.#maximumRecordTimeInternal);
}
}
this.#openNestableAsyncEvents.clear();
}
private addNestableAsyncEvent(event: Event): void {
const key = event.categoriesString + '.' + event.id;
let openEventsStack = this.#openNestableAsyncEvents.get(key);
switch (event.phase) {
case TraceEngine.Types.TraceEvents.Phase.ASYNC_NESTABLE_START: {
if (!openEventsStack) {
openEventsStack = [];
this.#openNestableAsyncEvents.set(key, openEventsStack);
}
const asyncEvent = new AsyncEvent(event);
openEventsStack.push(asyncEvent);
event.thread.addAsyncEvent(asyncEvent);
break;
}
case TraceEngine.Types.TraceEvents.Phase.ASYNC_NESTABLE_INSTANT: {
if (openEventsStack && openEventsStack.length) {
const event = openEventsStack[openEventsStack.length - 1];
if (event) {
event.addStep(event);
}
}
break;
}
case TraceEngine.Types.TraceEvents.Phase.ASYNC_NESTABLE_END: {
if (!openEventsStack || !openEventsStack.length) {
break;
}
const top = openEventsStack.pop();
if (!top) {
break;
}
if (top.name !== event.name) {
console.error(
`Begin/end event mismatch for nestable async event, ${top.name} vs. ${event.name}, key: ${key}`);
break;
}
top.addStep(event);
}
}
}
private addAsyncEvent(event: Event): void {
const key = event.categoriesString + '.' + event.name + '.' + event.id;
let asyncEvent = this.#openAsyncEvents.get(key);
if (event.phase === TraceEngine.Types.TraceEvents.Phase.ASYNC_BEGIN) {
if (asyncEvent) {
console.error(`Event ${event.name} has already been started`);
return;
}
asyncEvent = new AsyncEvent(event);
this.#openAsyncEvents.set(key, asyncEvent);
event.thread.addAsyncEvent(asyncEvent);
return;
}
if (!asyncEvent) {
// Quietly ignore stray async events, we're probably too late for the start.
return;
}
if (event.phase === TraceEngine.Types.TraceEvents.Phase.ASYNC_END) {
asyncEvent.addStep(event);
this.#openAsyncEvents.delete(key);
return;
}
if (event.phase === TraceEngine.Types.TraceEvents.Phase.ASYNC_STEP_INTO ||
event.phase === TraceEngine.Types.TraceEvents.Phase.ASYNC_STEP_PAST) {
const lastStep = asyncEvent.steps[asyncEvent.steps.length - 1];
if (lastStep && lastStep.phase !== TraceEngine.Types.TraceEvents.Phase.ASYNC_BEGIN &&
lastStep.phase !== event.phase) {
console.assert(
false,
'Async event step phase mismatch: ' + lastStep.phase + ' at ' + lastStep.startTime + ' vs. ' + event.phase +
' at ' + event.startTime);
return;
}
asyncEvent.addStep(event);
return;
}
console.assert(false, 'Invalid async event phase');
}
title(): string|undefined {
return this.#title;
}
parsedCategoriesForString(str: string): Set<string> {
let parsedCategories = this.#parsedCategories.get(str);
if (!parsedCategories) {
parsedCategories = new Set(str ? str.split(',') : []);
this.#parsedCategories.set(str, parsedCategories);
}
return parsedCategories;
}
}
export const eventPhasesOfInterestForTraceBounds: Set<TraceEngine.Types.TraceEvents.Phase> = new Set([
TraceEngine.Types.TraceEvents.Phase.BEGIN,
TraceEngine.Types.TraceEvents.Phase.END,
TraceEngine.Types.TraceEvents.Phase.COMPLETE,
TraceEngine.Types.TraceEvents.Phase.INSTANT,
]);
export const MetadataEvent = {
ProcessSortIndex: 'process_sort_index',
ProcessName: 'process_name',
ThreadSortIndex: 'thread_sort_index',
ThreadName: 'thread_name',
};
// TODO(alph): LegacyTopLevelEventCategory is not recorded since M74 and used for loading
// legacy profiles. Drop at some point.
export const LegacyTopLevelEventCategory = 'toplevel';
export const DevToolsMetadataEventCategory = 'disabled-by-default-devtools.timeline';
export const DevToolsTimelineEventCategory = 'disabled-by-default-devtools.timeline';
export abstract class BackingStorage {
appendString(_string: string): void {
}
abstract appendAccessibleString(string: string): () => Promise<string|null>;
finishWriting(): void {
}
reset(): void {
}
}
export function eventHasPayload(event: Event): event is PayloadEvent {
return 'rawPayload' in event;
}
export class Event {
categoriesString: string;
readonly #parsedCategories: Set<string>;
name: string;
phase: TraceEngine.Types.TraceEvents.Phase;
startTime: number;
thread: Thread;
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
args: any;
id!: string|null;
ordinal: number;
selfTime: number;
endTime?: number;
duration?: number;
// The constructor is protected so that we ensure that only classes or
// subclasses can directly instantiate events. All other callers should
// either create ConstructedEvent instances, which have a public constructor,
// or use the static fromPayload method which can create an event instance
// from the trace payload.
protected constructor(
categories: string|undefined, name: string, phase: TraceEngine.Types.TraceEvents.Phase, startTime: number,
thread: Thread) {
this.categoriesString = categories || '';
this.#parsedCategories = thread.getModel().parsedCategoriesForString(this.categoriesString);
this.name = name;
this.phase = phase;
this.startTime = startTime;
this.thread = thread;
this.args = {};
this.ordinal = 0;
this.selfTime = 0;
}
static compareStartTime(a: Event|null, b: Event|null): number {
if (!a || !b) {
return 0;
}
return a.startTime - b.startTime;
}
static orderedCompareStartTime(a: Event, b: Event): number {
// Array.mergeOrdered coalesces objects if comparator returns 0.
// To change this behavior this comparator return -1 in the case events
// startTime's are equal, so both events got placed into the result array.
return a.startTime - b.startTime || a.ordinal - b.ordinal || -1;
}
hasCategory(categoryName: string): boolean {
return this.#parsedCategories.has(categoryName);
}
setEndTime(endTime: number): void {
if (endTime < this.startTime) {
console.assert(false, 'Event out of order: ' + this.name);
return;
}
this.endTime = endTime;
this.duration = endTime - this.startTime;
}
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
addArgs(args: any): void {
// Shallow copy args to avoid modifying original #payload which may be saved to file.
for (const name in args) {
if (name in this.args) {
console.error('Same argument name (' + name + ') is used for begin and end phases of ' + this.name);
}
(this.args as IgnoreListArgs)[name] = (args as IgnoreListArgs)[name];
}
}
complete(endEvent: Event): void {
if (endEvent.args) {
this.addArgs(endEvent.args);
} else {
console.error('Missing mandatory event argument \'args\' at ' + endEvent.startTime);
}
this.setEndTime(endEvent.startTime);
}
setBackingStorage(_backingStorage: (() => Promise<string|null>)|null): void {
}
}
/**
* Represents a tracing event that is not directly linked to an individual
* object in the trace. We construct these events at times, particularly when
* building up the CPU profile data for JS Profiling.
**/
export class ConstructedEvent extends Event {
// Because the constructor of Event is marked as protected, but we want
// people to be able to create constructed events, we override the
// constructor here, even though we are only calling super, in order to mark
// it as public.
constructor(
categories: string|undefined, name: string, phase: TraceEngine.Types.TraceEvents.Phase, startTime: number,
thread: Thread) {
super(categories, name, phase, startTime, thread);
}
}
/**
* Represents a tracing event that has been created directly from an object in
* the trace file and therefore is guaranteed to have a payload associated with
* it. The only way to create these events is to use the static fromPayload
* method, which you must call with a payload.
**/
export class PayloadEvent extends Event {
#rawPayload: EventPayload;
/**
* Returns the raw payload that was used to create this event instance.
**/
rawLegacyPayload(): EventPayload {
return this.#rawPayload;
}
/**
* Returns the raw payload that was used to create this event instance, but
* returns it typed as the new engine's TraceEventArgs option.
**/
rawPayload(): TraceEngine.Types.TraceEvents.TraceEventData {
return this.#rawPayload as unknown as TraceEngine.Types.TraceEvents.TraceEventData;
}
protected constructor(
categories: string|undefined, name: string, phase: TraceEngine.Types.TraceEvents.Phase, startTime: number,
thread: Thread, rawPayload: EventPayload) {
super(categories, name, phase, startTime, thread);
this.#rawPayload = rawPayload;
}
static fromPayload(payload: EventPayload, thread: Thread): PayloadEvent {
const event = new PayloadEvent(payload.cat, payload.name, payload.ph, payload.ts / 1000, thread, payload);
event.#rawPayload = payload;
if (payload.args) {
event.addArgs(payload.args);
}
if (typeof payload.dur === 'number') {
event.setEndTime((payload.ts + payload.dur) / 1000);
}
const id = TracingModel.extractId(payload);
if (typeof id !== 'undefined') {
event.id = id;
}
return event;
}
}
export class ObjectSnapshot extends PayloadEvent {
#backingStorage: (() => Promise<string|null>)|null;
#objectPromiseInternal: Promise<ObjectSnapshot|null>|null;
private constructor(
category: string|undefined, name: string, startTime: number, thread: Thread, rawPayload: EventPayload) {
super(category, name, TraceEngine.Types.TraceEvents.Phase.OBJECT_SNAPSHOT, startTime, thread, rawPayload);
this.#backingStorage = null;
this.#objectPromiseInternal = null;
}
static override fromPayload(payload: EventPayload, thread: Thread): ObjectSnapshot {
const snapshot = new ObjectSnapshot(payload.cat, payload.name, payload.ts / 1000, thread, payload);
const id = TracingModel.extractId(payload);
if (typeof id !== 'undefined') {
snapshot.id = id;
}
if (!payload.args || !payload.args['snapshot']) {
console.error('Missing mandatory \'snapshot\' argument at ' + payload.ts / 1000);
return snapshot;
}
if (payload.args) {
snapshot.addArgs(payload.args);
}
return snapshot;
}
requestObject(callback: (arg0: ObjectSnapshot|null) => void): void {
const snapshot = this.args['snapshot'];
if (snapshot) {
callback((snapshot as ObjectSnapshot));
return;
}
const storage = this.#backingStorage;
if (storage) {
storage().then(onRead, callback.bind(null, null));
}
function onRead(result: string|null): void {
if (!result) {
callback(null);
return;
}
try {
const payload = JSON.parse(result);
callback(payload['args']['snapshot']);
} catch (e) {
Common.Console.Console.instance().error('Malformed event data in backing storage');
callback(null);
}
}
}
objectPromise(): Promise<ObjectSnapshot|null> {
if (!this.#objectPromiseInternal) {
this.#objectPromiseInternal = new Promise(this.requestObject.bind(this));
}
return this.#objectPromiseInternal;
}
override setBackingStorage(backingStorage: (() => Promise<string|null>)|null): void {
if (!backingStorage) {
return;
}
this.#backingStorage = backingStorage;
this.args = {};
}
}
export class AsyncEvent extends ConstructedEvent {
steps: Event[];
causedFrame: boolean;
constructor(startEvent: Event) {
super(startEvent.categoriesString, startEvent.name, startEvent.phase, startEvent.startTime, startEvent.thread);
this.addArgs(startEvent.args);
this.steps = [startEvent];
this.causedFrame = false;
}
addStep(event: Event): void {
this.steps.push(event);
if (event.phase === TraceEngine.Types.TraceEvents.Phase.ASYNC_END ||
event.phase === TraceEngine.Types.TraceEvents.Phase.ASYNC_NESTABLE_END) {
this.setEndTime(event.startTime);
// FIXME: ideally, we shouldn't do this, but this makes the logic of converting
// async console events to sync ones much simpler.
this.steps[0].setEndTime(event.startTime);
}
}
}
class ProfileEventsGroup {
children: Event[];
constructor(event: Event) {
this.children = [event];
}
addChild(event: Event): void {
this.children.push(event);
}
}
class NamedObject {
model: TracingModel;
readonly idInternal: number;
#nameInternal: string;
#sortIndex: number;
constructor(model: TracingModel, id: number) {
this.model = model;
this.idInternal = id;
this.#nameInternal = '';
this.#sortIndex = 0;
}
static sort<Item extends NamedObject>(array: Item[]): Item[] {
return array.sort((a, b) => {
return a.#sortIndex !== b.#sortIndex ? a.#sortIndex - b.#sortIndex : a.name().localeCompare(b.name());
});
}
setName(name: string): void {
this.#nameInternal = name;
}
name(): string {
return this.#nameInternal;
}
id(): number {
return this.idInternal;
}
setSortIndex(sortIndex: number): void {
this.#sortIndex = sortIndex;
}
getModel(): TracingModel {
return this.model;
}
}
export class Process extends NamedObject {
readonly threads: Map<number, Thread>;
readonly #threadByNameInternal: Map<string, Thread|null>;
constructor(model: TracingModel, id: number) {
super(model, id);
this.threads = new Map();
this.#threadByNameInternal = new Map();
}
threadById(id: number): Thread {
let thread = this.threads.get(id);
if (!thread) {
thread = new Thread(this, id);
this.threads.set(id, thread);
}
return thread;
}
threadByName(name: string): Thread|null {
return this.#threadByNameInternal.get(name) || null;
}
setThreadByName(name: string, thread: Thread): void {
this.#threadByNameInternal.set(name, thread);
}
addEvent(payload: EventPayload): Event|null {
return this.threadById(payload.tid).addEvent(payload);
}
sortedThreads(): Thread[] {
return NamedObject.sort([...this.threads.values()]);
}
}
export class Thread extends NamedObject {
readonly #processInternal: Process;
#eventsInternal: Event[];
readonly #asyncEventsInternal: AsyncEvent[];
#lastTopLevelEvent: Event|null;
constructor(process: Process, id: number) {
super(process.getModel(), id);
this.#processInternal = process;
this.#eventsInternal = [];
this.#asyncEventsInternal = [];
this.#lastTopLevelEvent = null;
}
/**
* Whilst we are in the middle of migrating to the new Phase enum, we need to
* be able to compare events with the legacy phase to the new enum. This method
* does this by casting the event phase to a string, ensuring we can compare it
* against either enum. Once the migration is complete (crbug.com/1417587), we
* will be able to use === to compare with no TS errors and this method can be
* removed.
**/
#eventMatchesPhase(event: Event, phase: TraceEngine.Types.TraceEvents.Phase): boolean {
return (event.phase as string) === phase;
}
tracingComplete(): void {
this.#asyncEventsInternal.sort(Event.compareStartTime);
this.#eventsInternal.sort(Event.compareStartTime);
const stack: Event[] = [];
const toDelete = new Set<number>();
for (let i = 0; i < this.#eventsInternal.length; ++i) {
const e = this.#eventsInternal[i];
e.ordinal = i;
if (this.#eventMatchesPhase(e, TraceEngine.Types.TraceEvents.Phase.END)) {
toDelete.add(i); // Mark for removal.
// Quietly ignore unbalanced close events, they're legit (we could have missed start one).
if (!stack.length) {
continue;
}
const top = stack.pop();
if (!top) {
continue;
}
if (top.name !== e.name || top.categoriesString !== e.categoriesString) {
console.error(
'B/E events mismatch at ' + top.startTime + ' (' + top.name + ') vs. ' + e.startTime + ' (' + e.name +
')');
} else {
top.complete(e);
}
} else if (this.#eventMatchesPhase(e, TraceEngine.Types.TraceEvents.Phase.BEGIN)) {
stack.push(e);
}
}
// Handle Begin events with no matching End.
// This commonly happens due to a bug in the trace machinery. See crbug.com/982252
while (stack.length) {
const event = stack.pop();
if (event) {
// Masquerade the event as Instant, so it's rendered to the user.
// The ideal fix is resolving crbug.com/1021571, but handling that without a perfetto migration appears prohibitive
event.phase = TraceEngine.Types.TraceEvents.Phase.INSTANT;
}
}
this.#eventsInternal = this.#eventsInternal.filter((_, idx) => !toDelete.has(idx));
}
addEvent(payload: EventPayload): Event|null {
const event = payload.ph === TraceEngine.Types.TraceEvents.Phase.OBJECT_SNAPSHOT ?
ObjectSnapshot.fromPayload(payload, this) :
PayloadEvent.fromPayload(payload, this);
if (TracingModel.isTopLevelEvent(event)) {
// Discard nested "top-level" events.
const lastTopLevelEvent = this.#lastTopLevelEvent;
if (lastTopLevelEvent && (lastTopLevelEvent.endTime || 0) > event.startTime) {
return null;
}
this.#lastTopLevelEvent = event;
}
this.#eventsInternal.push(event);
return event;
}
addAsyncEvent(asyncEvent: AsyncEvent): void {
this.#asyncEventsInternal.push(asyncEvent);
}
override setName(name: string): void {
super.setName(name);
this.#processInternal.setThreadByName(name, this);
}
process(): Process {
return this.#processInternal;
}
events(): Event[] {
return this.#eventsInternal;
}
asyncEvents(): AsyncEvent[] {
return this.#asyncEventsInternal;
}
removeEventsByName(name: string): Event[] {
const extracted: Event[] = [];
this.#eventsInternal = this.#eventsInternal.filter(e => {
if (!e) {
return false;
}
if (e.name !== name) {
return true;
}
extracted.push(e);
return false;
});
return extracted;
}
}
export interface TimesForEventMs {
startTime: TraceEngine.Types.Timing.MilliSeconds;
endTime?: TraceEngine.Types.Timing.MilliSeconds;
selfTime: TraceEngine.Types.Timing.MilliSeconds;
duration: TraceEngine.Types.Timing.MilliSeconds;
}
export function timesForEventInMilliseconds(event: Event|
TraceEngine.Types.TraceEvents.TraceEventData): TimesForEventMs {
if (event instanceof Event) {
return {
startTime: TraceEngine.Types.Timing.MilliSeconds(event.startTime),
endTime: event.endTime ? TraceEngine.Types.Timing.MilliSeconds(event.endTime) : undefined,
duration: TraceEngine.Types.Timing.MilliSeconds(event.duration || 0),
selfTime: TraceEngine.Types.Timing.MilliSeconds(event.selfTime),
};
}
const duration = event.dur ? TraceEngine.Helpers.Timing.microSecondsToMilliseconds(event.dur) :
TraceEngine.Types.Timing.MilliSeconds(0);
return {
startTime: TraceEngine.Helpers.Timing.microSecondsToMilliseconds(event.ts),
endTime: TraceEngine.Helpers.Timing.microSecondsToMilliseconds(
TraceEngine.Types.Timing.MicroSeconds(event.ts + (event.dur || 0))),
duration: event.dur ? TraceEngine.Helpers.Timing.microSecondsToMilliseconds(event.dur) :
TraceEngine.Types.Timing.MilliSeconds(0),
// TODO(crbug.com/1434599): Implement selfTime calculation for events
// from the new engine.
selfTime: duration,
};
}
// 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: CompatibleTraceEvent, category: string): boolean {
if (event instanceof Event) {
return event.hasCategory(category);
}
let parsedCategoriesForEvent = parsedCategories.get(event.cat);
if (!parsedCategoriesForEvent) {
parsedCategoriesForEvent = new Set(event.cat.split(',') || []);
}
return parsedCategoriesForEvent.has(category);
}
export function phaseForEvent(event: Event|
TraceEngine.Types.TraceEvents.TraceEventData): TraceEngine.Types.TraceEvents.Phase {
if (event instanceof Event) {
return event.phase;
}
return event.ph;
}
export function threadIDForEvent(event: Event|
TraceEngine.Types.TraceEvents.TraceEventData): TraceEngine.Types.TraceEvents.ThreadID {
if (event instanceof Event) {
return event.thread.idInternal as TraceEngine.Types.TraceEvents.ThreadID;
}
return event.tid;
}
export function eventIsFromNewEngine(event: CompatibleTraceEvent|
null): event is TraceEngine.Types.TraceEvents.TraceEventData {
return event !== null && !(event instanceof Event);
}
export type CompatibleTraceEvent = Event|TraceEngine.Types.TraceEvents.TraceEventData;