blob: ee8632fac91b758cf8b5f97f48a1ee7eba20ee1a [file] [log] [blame]
/*
* 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.
*/
export default class TracingModel {
/**
* @param {!BackingStorage} backingStorage
*/
constructor(backingStorage) {
this._backingStorage = backingStorage;
// Avoid extra reset of the storage as it's expensive.
this._firstWritePending = true;
/** @type {!Map<(number|string), !Process>} */
this._processById = new Map();
this._processByName = new Map();
this._minimumRecordTime = 0;
this._maximumRecordTime = 0;
this._devToolsMetadataEvents = [];
/** @type {!Array<!Event>} */
this._asyncEvents = [];
/** @type {!Map<string, !AsyncEvent>} */
this._openAsyncEvents = new Map();
/** @type {!Map<string, !Array<!AsyncEvent>>} */
this._openNestableAsyncEvents = new Map();
/** @type {!Map<string, !ProfileEventsGroup>} */
this._profileGroups = new Map();
/** @type {!Map<string, !Set<string>>} */
this._parsedCategories = new Map();
}
/**
* @param {string} phase
* @return {boolean}
*/
static isNestableAsyncPhase(phase) {
return phase === 'b' || phase === 'e' || phase === 'n';
}
/**
* @param {string} phase
* @return {boolean}
*/
static isAsyncBeginPhase(phase) {
return phase === 'S' || phase === 'b';
}
/**
* @param {string} phase
* @return {boolean}
*/
static isAsyncPhase(phase) {
return TracingModel.isNestableAsyncPhase(phase) || phase === 'S' || phase === 'T' || phase === 'F' || phase === 'p';
}
/**
* @param {string} phase
* @return {boolean}
*/
static isFlowPhase(phase) {
return phase === 's' || phase === 't' || phase === 'f';
}
/**
* @param {!Event} event
* @return {boolean}
*/
static isTopLevelEvent(event) {
return event.hasCategory(DevToolsTimelineEventCategory) && event.name === 'RunTask' ||
event.hasCategory(LegacyTopLevelEventCategory) ||
event.hasCategory(DevToolsMetadataEventCategory) &&
event.name === 'Program'; // Older timelines may have this instead of toplevel.
}
/**
* @param {!SDK.TracingManager.EventPayload} payload
* @return {string|undefined}
*/
static _extractId(payload) {
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.`);
}
/**
* @param {!TracingModel} tracingModel
* @return {?Thread}
*
* TODO: Move this to a better place. This is here just for convenience o
* re-use between modules. This really belongs to a higher level, since it
* is specific to chrome's usage of tracing.
*/
static browserMainThread(tracingModel) {
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.error('Failed to find browser main thread in trace, some timeline features may be unavailable');
return null;
}
/**
* @return {!Array.<!Event>}
*/
devToolsMetadataEvents() {
return this._devToolsMetadataEvents;
}
/**
* @param {!Array.<!SDK.TracingManager.EventPayload>} events
*/
addEvents(events) {
for (let i = 0; i < events.length; ++i) {
this._addEvent(events[i]);
}
}
tracingComplete() {
this._processPendingAsyncEvents();
this._backingStorage.appendString(this._firstWritePending ? '[]' : ']');
this._backingStorage.finishWriting();
this._firstWritePending = false;
for (const process of this._processById.values()) {
for (const thread of process._threads.values()) {
thread.tracingComplete();
}
}
}
dispose() {
if (!this._firstWritePending) {
this._backingStorage.reset();
}
}
/**
* @param {number} offset
*/
adjustTime(offset) {
this._minimumRecordTime += offset;
this._maximumRecordTime += offset;
for (const process of this._processById.values()) {
for (const thread of process._threads.values()) {
for (const event of thread.events()) {
event.startTime += offset;
if (typeof event.endTime === 'number') {
event.endTime += offset;
}
}
for (const event of thread.asyncEvents()) {
event.startTime += offset;
if (typeof event.endTime === 'number') {
event.endTime += offset;
}
}
}
}
}
/**
* @param {!SDK.TracingManager.EventPayload} payload
*/
_addEvent(payload) {
let process = this._processById.get(payload.pid);
if (!process) {
process = new Process(this, payload.pid);
this._processById.set(payload.pid, process);
}
const phase = Phase;
const eventsDelimiter = ',\n';
this._backingStorage.appendString(this._firstWritePending ? '[' : eventsDelimiter);
this._firstWritePending = false;
const stringPayload = JSON.stringify(payload);
const isAccessible = payload.ph === phase.SnapshotObject;
let backingStorage = null;
const keepStringsLessThan = 10000;
if (isAccessible && stringPayload.length > keepStringsLessThan) {
backingStorage = this._backingStorage.appendAccessibleString(stringPayload);
} else {
this._backingStorage.appendString(stringPayload);
}
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 && (!this._minimumRecordTime || timestamp < this._minimumRecordTime) &&
(payload.ph === phase.Begin || payload.ph === phase.Complete || payload.ph === phase.Instant)) {
this._minimumRecordTime = timestamp;
}
const endTimeStamp = (payload.ts + (payload.dur || 0)) / 1000;
this._maximumRecordTime = Math.max(this._maximumRecordTime, endTimeStamp);
const event = process._addEvent(payload);
if (!event) {
return;
}
if (payload.ph === 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 (TracingModel.isAsyncPhase(payload.ph)) {
this._asyncEvents.push(event);
}
event._setBackingStorage(backingStorage);
if (event.hasCategory(DevToolsMetadataEventCategory)) {
this._devToolsMetadataEvents.push(event);
}
if (payload.ph !== 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;
}
}
/**
* @param {!Event} event
*/
_addSampleEvent(event) {
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));
}
}
/**
* @param {!Event} event
* @return {?ProfileEventsGroup}
*/
profileGroup(event) {
return this._profileGroups.get(`${event.thread.process().id()}:${event.id}`) || null;
}
/**
* @return {number}
*/
minimumRecordTime() {
return this._minimumRecordTime;
}
/**
* @return {number}
*/
maximumRecordTime() {
return this._maximumRecordTime;
}
/**
* @return {!Array.<!Process>}
*/
sortedProcesses() {
return NamedObject._sort(this._processById.valuesArray());
}
/**
* @param {string} name
* @return {?Process}
*/
processByName(name) {
return this._processByName.get(name);
}
/**
* @param {number} pid
* @return {?Process}
*/
processById(pid) {
return this._processById.get(pid) || null;
}
/**
* @param {string} processName
* @param {string} threadName
* @return {?Thread}
*/
threadByName(processName, threadName) {
const process = this.processByName(processName);
return process && process.threadByName(threadName);
}
_processPendingAsyncEvents() {
this._asyncEvents.sort(Event.compareStartTime);
for (let i = 0; i < this._asyncEvents.length; ++i) {
const event = this._asyncEvents[i];
if (TracingModel.isNestableAsyncPhase(event.phase)) {
this._addNestableAsyncEvent(event);
} else {
this._addAsyncEvent(event);
}
}
this._asyncEvents = [];
this._closeOpenAsyncEvents();
}
_closeOpenAsyncEvents() {
for (const event of this._openAsyncEvents.values()) {
event.setEndTime(this._maximumRecordTime);
// FIXME: remove this once we figure a better way to convert async console
// events to sync [waterfall] timeline records.
event.steps[0].setEndTime(this._maximumRecordTime);
}
this._openAsyncEvents.clear();
for (const eventStack of this._openNestableAsyncEvents.values()) {
while (eventStack.length) {
eventStack.pop().setEndTime(this._maximumRecordTime);
}
}
this._openNestableAsyncEvents.clear();
}
/**
* @param {!Event} event
*/
_addNestableAsyncEvent(event) {
const phase = Phase;
const key = event.categoriesString + '.' + event.id;
let openEventsStack = this._openNestableAsyncEvents.get(key);
switch (event.phase) {
case phase.NestableAsyncBegin:
if (!openEventsStack) {
openEventsStack = [];
this._openNestableAsyncEvents.set(key, openEventsStack);
}
const asyncEvent = new AsyncEvent(event);
openEventsStack.push(asyncEvent);
event.thread._addAsyncEvent(asyncEvent);
break;
case phase.NestableAsyncInstant:
if (openEventsStack && openEventsStack.length) {
openEventsStack.peekLast()._addStep(event);
}
break;
case phase.NestableAsyncEnd:
if (!openEventsStack || !openEventsStack.length) {
break;
}
const top = openEventsStack.pop();
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);
}
}
/**
* @param {!Event} event
*/
_addAsyncEvent(event) {
const phase = Phase;
const key = event.categoriesString + '.' + event.name + '.' + event.id;
let asyncEvent = this._openAsyncEvents.get(key);
if (event.phase === phase.AsyncBegin) {
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 === phase.AsyncEnd) {
asyncEvent._addStep(event);
this._openAsyncEvents.delete(key);
return;
}
if (event.phase === phase.AsyncStepInto || event.phase === phase.AsyncStepPast) {
const lastStep = asyncEvent.steps.peekLast();
if (lastStep.phase !== phase.AsyncBegin && 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');
}
/**
* @return {!BackingStorage}
*/
backingStorage() {
return this._backingStorage;
}
/**
* @param {string} str
* @return {!Set<string>}
*/
_parsedCategoriesForString(str) {
let parsedCategories = this._parsedCategories.get(str);
if (!parsedCategories) {
parsedCategories = new Set(str ? str.split(',') : []);
this._parsedCategories.set(str, parsedCategories);
}
return parsedCategories;
}
}
/**
* @enum {string}
*/
export const Phase = {
Begin: 'B',
End: 'E',
Complete: 'X',
Instant: 'I',
AsyncBegin: 'S',
AsyncStepInto: 'T',
AsyncStepPast: 'p',
AsyncEnd: 'F',
NestableAsyncBegin: 'b',
NestableAsyncEnd: 'e',
NestableAsyncInstant: 'n',
FlowBegin: 's',
FlowStep: 't',
FlowEnd: 'f',
Metadata: 'M',
Counter: 'C',
Sample: 'P',
CreateObject: 'N',
SnapshotObject: 'O',
DeleteObject: 'D'
};
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';
/**
* @interface
*/
export class BackingStorage {
/**
* @param {string} string
*/
appendString(string) {
}
/**
* @param {string} string
* @return {function():!Promise.<?string>}
*/
appendAccessibleString(string) {
}
finishWriting() {
}
reset() {}
}
/**
* @unrestricted
*/
export class Event {
/**
* @param {string|undefined} categories
* @param {string} name
* @param {!Phase} phase
* @param {number} startTime
* @param {!Thread} thread
*/
constructor(categories, name, phase, startTime, thread) {
/** @type {string} */
this.categoriesString = categories || '';
/** @type {!Set<string>} */
this._parsedCategories = thread._model._parsedCategoriesForString(this.categoriesString);
/** @type {string} */
this.name = name;
/** @type {!Phase} */
this.phase = phase;
/** @type {number} */
this.startTime = startTime;
/** @type {!Thread} */
this.thread = thread;
/** @type {!Object} */
this.args = {};
/** @type {number} */
this.selfTime = 0;
}
/**
* @this {null}
* @param {!SDK.TracingManager.EventPayload} payload
* @param {!Thread} thread
* @return {!Event}
*/
static fromPayload(payload, thread) {
const event = new Event(payload.cat, payload.name, /** @type {!Phase} */ (payload.ph), payload.ts / 1000, thread);
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;
}
if (payload.bind_id) {
event.bind_id = payload.bind_id;
}
return event;
}
/**
* @param {!Event} a
* @param {!Event} b
* @return {number}
*/
static compareStartTime(a, b) {
return a.startTime - b.startTime;
}
/**
* @param {!Event} a
* @param {!Event} b
* @return {number}
*/
static orderedCompareStartTime(a, b) {
// 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;
}
/**
* @param {string} categoryName
* @return {boolean}
*/
hasCategory(categoryName) {
return this._parsedCategories.has(categoryName);
}
/**
* @param {number} endTime
*/
setEndTime(endTime) {
if (endTime < this.startTime) {
console.assert(false, 'Event out of order: ' + this.name);
return;
}
this.endTime = endTime;
this.duration = endTime - this.startTime;
}
/**
* @param {!Object} args
*/
addArgs(args) {
// 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[name] = args[name];
}
}
/**
* @param {!Event} endEvent
*/
_complete(endEvent) {
if (endEvent.args) {
this.addArgs(endEvent.args);
} else {
console.error('Missing mandatory event argument \'args\' at ' + endEvent.startTime);
}
this.setEndTime(endEvent.startTime);
}
/**
* @param {?function():!Promise.<?string>} backingStorage
*/
_setBackingStorage(backingStorage) {
}
}
export class ObjectSnapshot extends Event {
/**
* @param {string|undefined} category
* @param {string} name
* @param {number} startTime
* @param {!Thread} thread
*/
constructor(category, name, startTime, thread) {
super(category, name, Phase.SnapshotObject, startTime, thread);
/** @type {?function():!Promise<?string>} */
this._backingStorage = null;
/** @type {string} */
this.id;
/** @type {?Promise<?>} */
this._objectPromise = null;
}
/**
* @override
* @this {null}
* @param {!SDK.TracingManager.EventPayload} payload
* @param {!Thread} thread
* @return {!ObjectSnapshot}
*/
static fromPayload(payload, thread) {
const snapshot = new ObjectSnapshot(payload.cat, payload.name, payload.ts / 1000, thread);
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;
}
/**
* @param {function(?)} callback
*/
requestObject(callback) {
const snapshot = this.args['snapshot'];
if (snapshot) {
callback(snapshot);
return;
}
this._backingStorage().then(onRead, callback.bind(null, null));
/**
* @param {?string} result
*/
function onRead(result) {
if (!result) {
callback(null);
return;
}
try {
const payload = JSON.parse(result);
callback(payload['args']['snapshot']);
} catch (e) {
Common.console.error('Malformed event data in backing storage');
callback(null);
}
}
}
/**
* @return {!Promise<?>}
*/
objectPromise() {
if (!this._objectPromise) {
this._objectPromise = new Promise(this.requestObject.bind(this));
}
return this._objectPromise;
}
/**
* @override
* @param {?function():!Promise.<?>} backingStorage
*/
_setBackingStorage(backingStorage) {
if (!backingStorage) {
return;
}
this._backingStorage = backingStorage;
this.args = {};
}
}
/**
* @unrestricted
*/
export class AsyncEvent extends Event {
/**
* @param {!Event} startEvent
*/
constructor(startEvent) {
super(startEvent.categoriesString, startEvent.name, startEvent.phase, startEvent.startTime, startEvent.thread);
this.addArgs(startEvent.args);
this.steps = [startEvent];
}
/**
* @param {!Event} event
*/
_addStep(event) {
this.steps.push(event);
if (event.phase === Phase.AsyncEnd || event.phase === Phase.NestableAsyncEnd) {
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);
}
}
}
/**
* @unrestricted
*/
class ProfileEventsGroup {
/**
* @param {!Event} event
*/
constructor(event) {
/** @type {!Array<!Event>} */
this.children = [event];
}
/**
* @param {!Event} event
*/
_addChild(event) {
this.children.push(event);
}
}
class NamedObject {
/**
* @param {!TracingModel} model
* @param {number} id
*/
constructor(model, id) {
this._model = model;
this._id = id;
this._name = '';
this._sortIndex = 0;
}
/**
* @param {!Array.<!NamedObject>} array
*/
static _sort(array) {
/**
* @param {!NamedObject} a
* @param {!NamedObject} b
*/
function comparator(a, b) {
return a._sortIndex !== b._sortIndex ? a._sortIndex - b._sortIndex : a.name().localeCompare(b.name());
}
return array.sort(comparator);
}
/**
* @param {string} name
*/
_setName(name) {
this._name = name;
}
/**
* @return {string}
*/
name() {
return this._name;
}
/**
* @param {number} sortIndex
*/
_setSortIndex(sortIndex) {
this._sortIndex = sortIndex;
}
}
export class Process extends NamedObject {
/**
* @param {!TracingModel} model
* @param {number} id
*/
constructor(model, id) {
super(model, id);
/** @type {!Map<number, !Thread>} */
this._threads = new Map();
this._threadByName = new Map();
}
/**
* @return {number}
*/
id() {
return this._id;
}
/**
* @param {number} id
* @return {!Thread}
*/
threadById(id) {
let thread = this._threads.get(id);
if (!thread) {
thread = new Thread(this, id);
this._threads.set(id, thread);
}
return thread;
}
/**
* @param {string} name
* @return {?Thread}
*/
threadByName(name) {
return this._threadByName.get(name) || null;
}
/**
* @param {string} name
* @param {!Thread} thread
*/
_setThreadByName(name, thread) {
this._threadByName.set(name, thread);
}
/**
* @param {!SDK.TracingManager.EventPayload} payload
* @return {?Event} event
*/
_addEvent(payload) {
return this.threadById(payload.tid)._addEvent(payload);
}
/**
* @return {!Array.<!Thread>}
*/
sortedThreads() {
return NamedObject._sort(this._threads.valuesArray());
}
}
export class Thread extends NamedObject {
/**
* @param {!Process} process
* @param {number} id
*/
constructor(process, id) {
super(process._model, id);
this._process = process;
this._events = [];
this._asyncEvents = [];
this._lastTopLevelEvent = null;
}
tracingComplete() {
this._asyncEvents.sort(Event.compareStartTime);
this._events.sort(Event.compareStartTime);
const phases = Phase;
const stack = [];
for (let i = 0; i < this._events.length; ++i) {
const e = this._events[i];
e.ordinal = i;
switch (e.phase) {
case phases.End:
this._events[i] = null; // 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.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);
}
break;
case phases.Begin:
stack.push(e);
break;
}
}
while (stack.length) {
stack.pop().setEndTime(this._model.maximumRecordTime());
}
this._events.remove(null, false);
}
/**
* @param {!SDK.TracingManager.EventPayload} payload
* @return {?Event} event
*/
_addEvent(payload) {
const event = payload.ph === Phase.SnapshotObject ? ObjectSnapshot.fromPayload(payload, this) :
Event.fromPayload(payload, this);
if (TracingModel.isTopLevelEvent(event)) {
// Discard nested "top-level" events.
if (this._lastTopLevelEvent && this._lastTopLevelEvent.endTime > event.startTime) {
return null;
}
this._lastTopLevelEvent = event;
}
this._events.push(event);
return event;
}
/**
* @param {!AsyncEvent} asyncEvent
*/
_addAsyncEvent(asyncEvent) {
this._asyncEvents.push(asyncEvent);
}
/**
* @override
* @param {string} name
*/
_setName(name) {
super._setName(name);
this._process._setThreadByName(name, this);
}
/**
* @return {number}
*/
id() {
return this._id;
}
/**
* @return {!Process}
*/
process() {
return this._process;
}
/**
* @return {!Array.<!Event>}
*/
events() {
return this._events;
}
/**
* @return {!Array.<!AsyncEvent>}
*/
asyncEvents() {
return this._asyncEvents;
}
}
/* Legacy exported object */
self.SDK = self.SDK || {};
/* Legacy exported object */
SDK = SDK || {};
/** @constructor */
SDK.TracingModel = TracingModel;
SDK.TracingModel.Phase = Phase;
SDK.TracingModel.MetadataEvent = MetadataEvent;
SDK.TracingModel.LegacyTopLevelEventCategory = LegacyTopLevelEventCategory;
SDK.TracingModel.DevToolsMetadataEventCategory = DevToolsMetadataEventCategory;
SDK.TracingModel.DevToolsTimelineEventCategory = DevToolsTimelineEventCategory;
/** @constructor */
SDK.TracingModel.Event = Event;
/** @constructor */
SDK.TracingModel.ObjectSnapshot = ObjectSnapshot;
/** @constructor */
SDK.TracingModel.AsyncEvent = AsyncEvent;
/** @constructor */
SDK.TracingModel.Process = Process;
/** @constructor */
SDK.TracingModel.Thread = Thread;
/** @interface */
SDK.BackingStorage = BackingStorage;