| /* |
| * 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; |