blob: bd7d364ea8dbcc4d8bea822e9ed725667c00132a [file] [log] [blame]
/*
* Copyright (C) 2014 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/* eslint-disable rulesdir/no_underscored_properties */
import * as Common from '../core/common/common.js';
import * as Host from '../core/host/host.js';
import * as i18n from '../core/i18n/i18n.js';
import * as Platform from '../core/platform/platform.js';
import * as Root from '../core/root/root.js';
import * as SDK from '../core/sdk/sdk.js';
import * as Bindings from '../models/bindings/bindings.js';
import * as PerfUI from '../perf_ui/perf_ui.js';
import * as ThemeSupport from '../theme_support/theme_support.js';
import * as TimelineModel from '../timeline_model/timeline_model.js';
import * as UI from '../ui/legacy/legacy.js';
import {PerformanceModel} from './PerformanceModel.js'; // eslint-disable-line no-unused-vars
import {FlameChartStyle, Selection, TimelineFlameChartMarker} from './TimelineFlameChartView.js';
import {TimelineSelection} from './TimelinePanel.js';
import {TimelineCategory, TimelineUIUtils} from './TimelineUIUtils.js'; // eslint-disable-line no-unused-vars
const UIStrings = {
/**
*@description Text in Timeline Flame Chart Data Provider of the Performance panel
*/
onIgnoreList: 'On ignore list',
/**
*@description Text in Timeline Flame Chart Data Provider of the Performance panel
*/
input: 'Input',
/**
*@description Text that refers to the animation of the web page
*/
animation: 'Animation',
/**
*@description Text in Timeline Flame Chart Data Provider of the Performance panel
*/
timings: 'Timings',
/**
*@description Title of the Console tool
*/
console: 'Console',
/**
*@description Text in Timeline Flame Chart Data Provider of the Performance panel
*@example {example.com} PH1
*/
mainS: 'Main — {PH1}',
/**
*@description Text that refers to the main target
*/
main: 'Main',
/**
*@description Text in Timeline Flame Chart Data Provider of the Performance panel
*@example {https://example.com} PH1
*/
frameS: 'Frame — {PH1}',
/**
*@description Text in Timeline Flame Chart Data Provider of the Performance panel
*/
subframe: 'Subframe',
/**
*@description Text in Timeline Flame Chart Data Provider of the Performance panel
*/
raster: 'Raster',
/**
*@description Text in Timeline Flame Chart Data Provider of the Performance panel
*@example {2} PH1
*/
rasterizerThreadS: 'Rasterizer Thread {PH1}',
/**
*@description Text in Timeline Flame Chart Data Provider of the Performance panel
*/
gpu: 'GPU',
/**
*@description Text in Timeline Flame Chart Data Provider of the Performance panel
*/
thread: 'Thread',
/**
*@description Text in Timeline for the Experience title
*/
experience: 'Experience',
/**
*@description Text in Timeline Flame Chart Data Provider of the Performance panel
*/
interactions: 'Interactions',
/**
*@description Text for rendering frames
*/
frames: 'Frames',
/**
* @description Text in the Performance panel to show how long was spent in a particular part of the code.
* The first placeholder is the total time taken for this node and all children, the second is the self time
* (time taken in this node, without children included).
*@example {10ms} PH1
*@example {10ms} PH2
*/
sSelfS: '{PH1} (self {PH2})',
/**
*@description Tooltip text for the number of CLS occurences in Timeline
*@example {4} PH1
*/
occurrencesS: 'Occurrences: {PH1}',
/**
*@description Text in Timeline Flame Chart Data Provider of the Performance panel
*@example {10ms} PH1
*@example {100.0} PH2
*/
sFfps: '{PH1} ~ {PH2} fps',
/**
*@description Text in Timeline Flame Chart Data Provider of the Performance panel
*/
idleFrame: 'Idle Frame',
/**
*@description Text in Timeline Frame Chart Data Provider of the Performance panel
*/
droppedFrame: 'Dropped Frame',
/**
*@description Text for a rendering frame
*/
frame: 'Frame',
/**
*@description Warning text content in Timeline Flame Chart Data Provider of the Performance panel
*/
longFrame: 'Long frame',
/**
* @description Text for the name of a thread of the page. Used when there are multiple threads but
* a more specific name for this thread is not available. The placeholder is a number that uniquely
* identifies this thread.
* @example {1} PH1
*/
threadS: 'Thread {PH1}',
};
const str_ = i18n.i18n.registerUIStrings('timeline/TimelineFlameChartDataProvider.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export class TimelineFlameChartDataProvider extends Common.ObjectWrapper.ObjectWrapper implements
PerfUI.FlameChart.FlameChartDataProvider {
_font: string;
_timelineData: PerfUI.FlameChart.TimelineData|null;
_currentLevel: number;
_performanceModel: PerformanceModel|null;
_model: TimelineModel.TimelineModel.TimelineModelImpl|null;
_minimumBoundary: number;
_maximumBoundary: number;
_timeSpan: number;
_consoleColorGenerator: Common.Color.Generator;
_extensionColorGenerator: Common.Color.Generator;
_headerLevel1: PerfUI.FlameChart.GroupStyle;
_headerLevel2: PerfUI.FlameChart.GroupStyle;
_staticHeader: PerfUI.FlameChart.GroupStyle;
_framesHeader: PerfUI.FlameChart.GroupStyle;
_collapsibleTimingsHeader: PerfUI.FlameChart.GroupStyle;
_timingsHeader: PerfUI.FlameChart.GroupStyle;
_screenshotsHeader: PerfUI.FlameChart.GroupStyle;
_interactionsHeaderLevel1: PerfUI.FlameChart.GroupStyle;
_interactionsHeaderLevel2: PerfUI.FlameChart.GroupStyle;
_experienceHeader: PerfUI.FlameChart.GroupStyle;
_flowEventIndexById: Map<string, number>;
_entryData!: (SDK.FilmStripModel.Frame|SDK.TracingModel.Event|
TimelineModel.TimelineFrameModel.TimelineFrame|TimelineModel.TimelineIRModel.Phases)[];
_entryTypeByLevel!: EntryType[];
_markers!: TimelineFlameChartMarker[];
_asyncColorByInteractionPhase!: Map<TimelineModel.TimelineIRModel.Phases, string>;
_screenshotImageCache!: Map<SDK.FilmStripModel.Frame, HTMLImageElement|null>;
_extensionInfo!: {
title: string,
model: SDK.TracingModel.TracingModel,
}[];
_entryIndexToTitle!: string[];
_asyncColorByCategory!: Map<TimelineCategory, string>;
_lastInitiatorEntry!: number;
_entryParent!: SDK.TracingModel.Event[];
_frameGroup?: PerfUI.FlameChart.Group;
_lastSelection?: Selection;
_colorForEvent?: ((arg0: SDK.TracingModel.Event) => string);
constructor() {
super();
this.reset();
this._font = '11px ' + Host.Platform.fontFamily();
this._timelineData = null;
this._currentLevel = 0;
this._performanceModel = null;
this._model = null;
this._minimumBoundary = 0;
this._maximumBoundary = 0;
this._timeSpan = 0;
this._consoleColorGenerator = new Common.Color.Generator(
{
min: 30,
max: 55,
count: undefined,
},
{min: 70, max: 100, count: 6}, 50, 0.7);
this._extensionColorGenerator = new Common.Color.Generator(
{
min: 210,
max: 300,
count: undefined,
},
{min: 70, max: 100, count: 6}, 70, 0.7);
this._headerLevel1 = this._buildGroupStyle({shareHeaderLine: false});
this._headerLevel2 = this._buildGroupStyle({padding: 2, nestingLevel: 1, collapsible: false});
this._staticHeader = this._buildGroupStyle({collapsible: false});
this._framesHeader = this._buildGroupStyle({useFirstLineForOverview: true});
this._collapsibleTimingsHeader =
this._buildGroupStyle({shareHeaderLine: true, useFirstLineForOverview: true, collapsible: true});
this._timingsHeader =
this._buildGroupStyle({shareHeaderLine: true, useFirstLineForOverview: true, collapsible: false});
this._screenshotsHeader =
this._buildGroupStyle({useFirstLineForOverview: true, nestingLevel: 1, collapsible: false, itemsHeight: 150});
this._interactionsHeaderLevel1 = this._buildGroupStyle({useFirstLineForOverview: true});
this._interactionsHeaderLevel2 = this._buildGroupStyle({padding: 2, nestingLevel: 1});
this._experienceHeader = this._buildGroupStyle({collapsible: false});
this._flowEventIndexById = new Map();
}
_buildGroupStyle(extra: Object): PerfUI.FlameChart.GroupStyle {
const defaultGroupStyle = {
padding: 4,
height: 17,
collapsible: true,
color:
ThemeSupport.ThemeSupport.instance().patchColorText('#222', ThemeSupport.ThemeSupport.ColorUsage.Foreground),
backgroundColor:
ThemeSupport.ThemeSupport.instance().patchColorText('white', ThemeSupport.ThemeSupport.ColorUsage.Background),
font: this._font,
nestingLevel: 0,
shareHeaderLine: true,
};
return /** @type {!PerfUI.FlameChart.GroupStyle} */ Object.assign(defaultGroupStyle, extra) as
PerfUI.FlameChart.GroupStyle;
}
setModel(performanceModel: PerformanceModel|null): void {
this.reset();
this._performanceModel = performanceModel;
this._model = performanceModel && performanceModel.timelineModel();
}
groupTrack(group: PerfUI.FlameChart.Group): TimelineModel.TimelineModel.Track|null {
return group.track || null;
}
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
navStartTimes(): Map<any, any> {
if (!this._model) {
return new Map();
}
return this._model.navStartTimes();
}
entryTitle(entryIndex: number): string|null {
const entryTypes = EntryType;
const entryType = this._entryType(entryIndex);
if (entryType === entryTypes.Event) {
const event = (this._entryData[entryIndex] as SDK.TracingModel.Event);
if (event.phase === SDK.TracingModel.Phase.AsyncStepInto ||
event.phase === SDK.TracingModel.Phase.AsyncStepPast) {
return event.name + ':' + event.args['step'];
}
if (eventToDisallowRoot.get(event)) {
return i18nString(UIStrings.onIgnoreList);
}
if (this._performanceModel && this._performanceModel.timelineModel().isMarkerEvent(event)) {
return TimelineUIUtils.markerShortTitle(event);
}
return TimelineUIUtils.eventTitle(event);
}
if (entryType === entryTypes.ExtensionEvent) {
const event = (this._entryData[entryIndex] as SDK.TracingModel.Event);
return event.name;
}
if (entryType === entryTypes.Screenshot) {
return '';
}
let title: Common.UIString.LocalizedString|string = this._entryIndexToTitle[entryIndex];
if (!title) {
title = `Unexpected entryIndex ${entryIndex}`;
console.error(title);
}
return title;
}
textColor(index: number): string {
const event = this._entryData[index];
return event && eventToDisallowRoot.get((event as SDK.TracingModel.Event)) ? '#888' : FlameChartStyle.textColor;
}
entryFont(_index: number): string|null {
return this._font;
}
reset(): void {
this._currentLevel = 0;
this._timelineData = null;
this._entryData = [];
this._entryParent = [];
this._entryTypeByLevel = [];
this._entryIndexToTitle = [];
this._markers = [];
this._asyncColorByCategory = new Map();
this._asyncColorByInteractionPhase = new Map();
this._extensionInfo = [];
this._screenshotImageCache = new Map();
}
maxStackDepth(): number {
return this._currentLevel;
}
timelineData(): PerfUI.FlameChart.TimelineData {
if (this._timelineData) {
return this._timelineData;
}
this._timelineData = new PerfUI.FlameChart.TimelineData([], [], [], []);
if (!this._model) {
return this._timelineData;
}
this._flowEventIndexById.clear();
this._minimumBoundary = this._model.minimumRecordTime();
this._timeSpan = this._model.isEmpty() ? 1000 : this._model.maximumRecordTime() - this._minimumBoundary;
this._currentLevel = 0;
if (this._model.isGenericTrace()) {
this._processGenericTrace();
} else {
this._processInspectorTrace();
}
return this._timelineData;
}
_processGenericTrace(): void {
const processGroupStyle = this._buildGroupStyle({shareHeaderLine: false});
const threadGroupStyle = this._buildGroupStyle({padding: 2, nestingLevel: 1, shareHeaderLine: false});
const eventEntryType = EntryType.Event;
const tracksByProcess =
new Platform.MapUtilities.Multimap<SDK.TracingModel.Process, TimelineModel.TimelineModel.Track>();
if (!this._model) {
return;
}
for (const track of this._model.tracks()) {
if (track.thread !== null) {
tracksByProcess.set(track.thread.process(), track);
} else {
// The Timings track can reach this point, so we should probably do something more useful.
console.error('Failed to process track');
}
}
for (const process of tracksByProcess.keysArray()) {
if (tracksByProcess.size > 1) {
const name = `${process.name()} ${process.id()}`;
this._appendHeader(name, processGroupStyle, false /* selectable */);
}
for (const track of tracksByProcess.get(process)) {
const group = this._appendSyncEvents(
track, track.events, track.name, threadGroupStyle, eventEntryType, true /* selectable */);
if (this._timelineData &&
(!this._timelineData.selectedGroup ||
track.name === TimelineModel.TimelineModel.TimelineModelImpl.BrowserMainThreadName)) {
this._timelineData.selectedGroup = group;
}
}
}
}
_processInspectorTrace(): void {
this._appendFrames();
this._appendInteractionRecords();
const eventEntryType = EntryType.Event;
const weight = (track: TimelineModel.TimelineModel.Track): 0|1|2|3|4|5|6|7|8|9|10|- 1 => {
switch (track.type) {
case TimelineModel.TimelineModel.TrackType.Input:
return 0;
case TimelineModel.TimelineModel.TrackType.Animation:
return 1;
case TimelineModel.TimelineModel.TrackType.Timings:
return 2;
case TimelineModel.TimelineModel.TrackType.Console:
return 3;
case TimelineModel.TimelineModel.TrackType.Experience:
return 4;
case TimelineModel.TimelineModel.TrackType.MainThread:
return track.forMainFrame ? 5 : 6;
case TimelineModel.TimelineModel.TrackType.Worker:
return 7;
case TimelineModel.TimelineModel.TrackType.Raster:
return 8;
case TimelineModel.TimelineModel.TrackType.GPU:
return 9;
case TimelineModel.TimelineModel.TrackType.Other:
return 10;
default:
return -1;
}
};
if (!this._model) {
return;
}
const tracks = this._model.tracks().slice();
tracks.sort((a, b) => weight(a) - weight(b));
let rasterCount = 0;
for (const track of tracks) {
switch (track.type) {
case TimelineModel.TimelineModel.TrackType.Input: {
this._appendAsyncEventsGroup(
track, i18nString(UIStrings.input), track.asyncEvents, this._interactionsHeaderLevel2, eventEntryType,
false /* selectable */);
break;
}
case TimelineModel.TimelineModel.TrackType.Animation: {
this._appendAsyncEventsGroup(
track, i18nString(UIStrings.animation), track.asyncEvents, this._interactionsHeaderLevel2, eventEntryType,
false /* selectable */);
break;
}
case TimelineModel.TimelineModel.TrackType.Timings: {
const style = track.asyncEvents.length > 0 ? this._collapsibleTimingsHeader : this._timingsHeader;
const group = this._appendHeader(i18nString(UIStrings.timings), style, true /* selectable */);
group.track = track;
this._appendPageMetrics();
this._copyPerfMarkEvents(track);
this._appendSyncEvents(track, track.events, null, null, eventEntryType, true /* selectable */);
this._appendAsyncEventsGroup(track, null, track.asyncEvents, null, eventEntryType, true /* selectable */);
break;
}
case TimelineModel.TimelineModel.TrackType.Console: {
this._appendAsyncEventsGroup(
track, i18nString(UIStrings.console), track.asyncEvents, this._headerLevel1, eventEntryType,
true /* selectable */);
break;
}
case TimelineModel.TimelineModel.TrackType.MainThread: {
if (track.forMainFrame) {
const group = this._appendSyncEvents(
track, track.events,
track.url ? i18nString(UIStrings.mainS, {PH1: track.url}) : i18nString(UIStrings.main),
this._headerLevel1, eventEntryType, true /* selectable */);
if (group && this._timelineData) {
this._timelineData.selectedGroup = group;
}
} else {
this._appendSyncEvents(
track, track.events,
track.url ? i18nString(UIStrings.frameS, {PH1: track.url}) : i18nString(UIStrings.subframe),
this._headerLevel1, eventEntryType, true /* selectable */);
}
break;
}
case TimelineModel.TimelineModel.TrackType.Worker: {
this._appendSyncEvents(
track, track.events, track.name, this._headerLevel1, eventEntryType, true /* selectable */);
break;
}
case TimelineModel.TimelineModel.TrackType.Raster: {
if (!rasterCount) {
this._appendHeader(i18nString(UIStrings.raster), this._headerLevel1, false /* selectable */);
}
++rasterCount;
this._appendSyncEvents(
track, track.events, i18nString(UIStrings.rasterizerThreadS, {PH1: rasterCount}), this._headerLevel2,
eventEntryType, true /* selectable */);
break;
}
case TimelineModel.TimelineModel.TrackType.GPU: {
this._appendSyncEvents(
track, track.events, i18nString(UIStrings.gpu), this._headerLevel1, eventEntryType,
true /* selectable */);
break;
}
case TimelineModel.TimelineModel.TrackType.Other: {
this._appendSyncEvents(
track, track.events, track.name || i18nString(UIStrings.thread), this._headerLevel1, eventEntryType,
true /* selectable */);
this._appendAsyncEventsGroup(
track, track.name, track.asyncEvents, this._headerLevel1, eventEntryType, true /* selectable */);
break;
}
case TimelineModel.TimelineModel.TrackType.Experience: {
this._appendSyncEvents(
track, track.events, i18nString(UIStrings.experience), this._experienceHeader, eventEntryType,
true /* selectable */);
break;
}
}
}
if (this._timelineData && this._timelineData.selectedGroup) {
this._timelineData.selectedGroup.expanded = true;
}
for (let extensionIndex = 0; extensionIndex < this._extensionInfo.length; extensionIndex++) {
this._innerAppendExtensionEvents(extensionIndex);
}
this._markers.sort((a, b) => a.startTime() - b.startTime());
if (this._timelineData) {
this._timelineData.markers = this._markers;
}
this._flowEventIndexById.clear();
}
minimumBoundary(): number {
return this._minimumBoundary;
}
totalTime(): number {
return this._timeSpan;
}
search(startTime: number, endTime: number, filter: TimelineModel.TimelineModelFilter.TimelineModelFilter): number[] {
const result = [];
const entryTypes = EntryType;
this.timelineData();
for (let i = 0; i < this._entryData.length; ++i) {
if (this._entryType(i) !== entryTypes.Event) {
continue;
}
const event = (this._entryData[i] as SDK.TracingModel.Event);
if (event.startTime > endTime) {
continue;
}
if ((event.endTime || event.startTime) < startTime) {
continue;
}
if (filter.accept(event)) {
result.push(i);
}
}
result.sort(
(a, b) => SDK.TracingModel.Event.compareStartTime(
(this._entryData[a] as SDK.TracingModel.Event), (this._entryData[b] as SDK.TracingModel.Event)));
return result;
}
_appendSyncEvents(
track: TimelineModel.TimelineModel.Track|null, events: SDK.TracingModel.Event[], title: string|null,
style: PerfUI.FlameChart.GroupStyle|null, entryType: EntryType, selectable: boolean): PerfUI.FlameChart.Group
|null {
if (!events.length) {
return null;
}
if (!this._performanceModel || !this._model) {
return null;
}
const isExtension = entryType === EntryType.ExtensionEvent;
const openEvents = [];
const ignoreListingEnabled = !isExtension && Root.Runtime.experiments.isEnabled('blackboxJSFramesOnTimeline');
let maxStackDepth = 0;
let group: PerfUI.FlameChart.Group|null = null;
if (track && track.type === TimelineModel.TimelineModel.TrackType.MainThread) {
group = this._appendHeader((title as string), (style as PerfUI.FlameChart.GroupStyle), selectable);
group.track = track;
}
for (let i = 0; i < events.length; ++i) {
const e = events[i];
// Skip Layout Shifts and TTI events when dealing with the main thread.
if (this._performanceModel) {
const isInteractiveTime = this._performanceModel.timelineModel().isInteractiveTimeEvent(e);
const isLayoutShift = this._performanceModel.timelineModel().isLayoutShiftEvent(e);
const skippableEvent = isInteractiveTime || isLayoutShift;
if (track && track.type === TimelineModel.TimelineModel.TrackType.MainThread && skippableEvent) {
continue;
}
}
if (this._performanceModel && this._performanceModel.timelineModel().isLayoutShiftEvent(e)) {
// Expand layout shift events to the size of the frame in which it is situated.
for (const frame of this._performanceModel.frames()) {
// Locate the correct frame and expand the event accordingly.
if (typeof e.endTime === 'undefined') {
e.setEndTime(e.startTime);
}
const isAfterStartTime = e.startTime >= frame.startTime;
const isBeforeEndTime = e.endTime && e.endTime <= frame.endTime;
const eventIsInFrame = isAfterStartTime && isBeforeEndTime;
if (!eventIsInFrame) {
continue;
}
e.startTime = frame.startTime;
e.setEndTime(frame.endTime);
}
}
if (!isExtension && this._performanceModel.timelineModel().isMarkerEvent(e)) {
this._markers.push(new TimelineFlameChartMarker(
e.startTime, e.startTime - this._model.minimumRecordTime(), TimelineUIUtils.markerStyleForEvent(e)));
}
if (!SDK.TracingModel.TracingModel.isFlowPhase(e.phase)) {
if (!e.endTime && e.phase !== SDK.TracingModel.Phase.Instant) {
continue;
}
if (SDK.TracingModel.TracingModel.isAsyncPhase(e.phase)) {
continue;
}
if (!isExtension && !this._performanceModel.isVisible(e)) {
continue;
}
}
while (openEvents.length &&
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// @ts-expect-error
((openEvents[openEvents.length - 1] as SDK.TracingModel.Event).endTime) <= e.startTime) {
openEvents.pop();
}
eventToDisallowRoot.set(e, false);
if (ignoreListingEnabled && this._isIgnoreListedEvent(e)) {
const parent = openEvents[openEvents.length - 1];
if (parent && eventToDisallowRoot.get(parent)) {
continue;
}
eventToDisallowRoot.set(e, true);
}
if (!group && title) {
group = this._appendHeader(title, (style as PerfUI.FlameChart.GroupStyle), selectable);
if (selectable) {
group.track = track;
}
}
const level = this._currentLevel + openEvents.length;
const index = this._appendEvent(e, level);
if (openEvents.length) {
this._entryParent[index] = (openEvents[openEvents.length - 1] as SDK.TracingModel.Event);
}
if (!isExtension && this._performanceModel.timelineModel().isMarkerEvent(e)) {
// @ts-ignore This is invalid code, but we should keep it for now
this._timelineData.entryTotalTimes[this._entryData.length] = undefined;
}
maxStackDepth = Math.max(maxStackDepth, openEvents.length + 1);
if (e.endTime) {
openEvents.push(e);
}
}
this._entryTypeByLevel.length = this._currentLevel + maxStackDepth;
this._entryTypeByLevel.fill(entryType, this._currentLevel);
this._currentLevel += maxStackDepth;
return group;
}
_isIgnoreListedEvent(event: SDK.TracingModel.Event): boolean {
if (event.name !== TimelineModel.TimelineModel.RecordType.JSFrame) {
return false;
}
const url = event.args['data']['url'];
return url && this._isIgnoreListedURL(url);
}
_isIgnoreListedURL(url: string): boolean {
return Bindings.IgnoreListManager.IgnoreListManager.instance().isIgnoreListedURL(url);
}
_appendAsyncEventsGroup(
track: TimelineModel.TimelineModel.Track|null, title: string|null, events: SDK.TracingModel.AsyncEvent[],
style: PerfUI.FlameChart.GroupStyle|null, entryType: EntryType, selectable: boolean): PerfUI.FlameChart.Group
|null {
if (!events.length) {
return null;
}
const lastUsedTimeByLevel: number[] = [];
let group: PerfUI.FlameChart.Group|null = null;
for (let i = 0; i < events.length; ++i) {
const asyncEvent = events[i];
if (!this._performanceModel || !this._performanceModel.isVisible(asyncEvent)) {
continue;
}
if (!group && title) {
group = this._appendHeader(title, (style as PerfUI.FlameChart.GroupStyle), selectable);
if (selectable) {
group.track = track;
}
}
const startTime = asyncEvent.startTime;
let level;
for (level = 0; level < lastUsedTimeByLevel.length && lastUsedTimeByLevel[level] > startTime; ++level) {
}
this._appendAsyncEvent(asyncEvent, this._currentLevel + level);
lastUsedTimeByLevel[level] = (asyncEvent.endTime as number);
}
this._entryTypeByLevel.length = this._currentLevel + lastUsedTimeByLevel.length;
this._entryTypeByLevel.fill(entryType, this._currentLevel);
this._currentLevel += lastUsedTimeByLevel.length;
return group;
}
_appendInteractionRecords(): void {
if (!this._performanceModel) {
return;
}
const interactionRecords = this._performanceModel.interactionRecords();
if (!interactionRecords.length) {
return;
}
this._appendHeader(i18nString(UIStrings.interactions), this._interactionsHeaderLevel1, false /* selectable */);
for (const segment of interactionRecords) {
const index = this._entryData.length;
this._entryData.push((segment.data as TimelineModel.TimelineIRModel.Phases));
this._entryIndexToTitle[index] = (segment.data as string);
if (this._timelineData) {
this._timelineData.entryLevels[index] = this._currentLevel;
this._timelineData.entryTotalTimes[index] = segment.end - segment.begin;
this._timelineData.entryStartTimes[index] = segment.begin;
}
}
this._entryTypeByLevel[this._currentLevel++] = EntryType.InteractionRecord;
}
_appendPageMetrics(): void {
this._entryTypeByLevel[this._currentLevel] = EntryType.Event;
if (!this._performanceModel || !this._model) {
return;
}
const metricEvents: SDK.TracingModel.Event[] = [];
const lcpEvents = [];
const timelineModel = this._performanceModel.timelineModel();
for (const track of this._model.tracks()) {
for (const event of track.events) {
if (!timelineModel.isMarkerEvent(event)) {
continue;
}
if (timelineModel.isLCPCandidateEvent(event) || timelineModel.isLCPInvalidateEvent(event)) {
lcpEvents.push(event);
} else {
metricEvents.push(event);
}
}
}
// Only the LCP event with the largest candidate index is relevant.
// Do not record an LCP event if it is an invalidate event.
if (lcpEvents.length > 0) {
const lcpEventsByNavigationId = new Map<string, SDK.TracingModel.Event>();
for (const e of lcpEvents) {
const key = e.args['data']['navigationId'];
const previousLastEvent = lcpEventsByNavigationId.get(key);
if (!previousLastEvent || previousLastEvent.args['data']['candidateIndex'] < e.args['data']['candidateIndex']) {
lcpEventsByNavigationId.set(key, e);
}
}
const latestCandidates = Array.from(lcpEventsByNavigationId.values());
const latestEvents = latestCandidates.filter(e => timelineModel.isLCPCandidateEvent(e));
metricEvents.push(...latestEvents);
}
metricEvents.sort(SDK.TracingModel.Event.compareStartTime);
if (this._timelineData) {
const totalTimes = this._timelineData.entryTotalTimes;
for (const event of metricEvents) {
this._appendEvent(event, this._currentLevel);
totalTimes[totalTimes.length - 1] = Number.NaN;
}
}
++this._currentLevel;
}
/**
* This function pushes a copy of each performance.mark() event from the Main track
* into Timings so they can be appended to the performance UI.
* Performance.mark() are a part of the "blink.user_timing" category alongside
* Navigation and Resource Timing events, so we must filter them out before pushing.
*/
_copyPerfMarkEvents(timingTrack: TimelineModel.TimelineModel.Track|null): void {
this._entryTypeByLevel[this._currentLevel] = EntryType.Event;
if (!this._performanceModel || !this._model || !timingTrack) {
return;
}
const timelineModel = this._performanceModel.timelineModel();
const ResourceTimingNames = [
'workerStart',
'redirectStart',
'redirectEnd',
'fetchStart',
'domainLookupStart',
'domainLookupEnd',
'connectStart',
'connectEnd',
'secureConnectionStart',
'requestStart',
'responseStart',
'responseEnd',
];
const NavTimingNames = [
'navigationStart',
'unloadEventStart',
'unloadEventEnd',
'redirectStart',
'redirectEnd',
'fetchStart',
'domainLookupStart',
'domainLookupEnd',
'connectStart',
'connectEnd',
'secureConnectionStart',
'requestStart',
'responseStart',
'responseEnd',
'domLoading',
'domInteractive',
'domContentLoadedEventStart',
'domContentLoadedEventEnd',
'domComplete',
'loadEventStart',
'loadEventEnd',
];
const IgnoreNames = [...ResourceTimingNames, ...NavTimingNames];
for (const track of this._model.tracks()) {
if (track.type === TimelineModel.TimelineModel.TrackType.MainThread) {
for (const event of track.events) {
if (timelineModel.isUserTimingEvent(event)) {
if (IgnoreNames.includes(event.name)) {
continue;
}
if (SDK.TracingModel.TracingModel.isAsyncPhase(event.phase)) {
continue;
}
event.setEndTime(event.startTime);
timingTrack.events.push(event);
}
}
}
}
++this._currentLevel;
}
_appendFrames(): void {
if (!this._performanceModel || !this._timelineData || !this._model) {
return;
}
const screenshots = this._performanceModel.filmStripModel().frames();
const hasFilmStrip = Boolean(screenshots.length);
this._framesHeader.collapsible = hasFilmStrip;
this._appendHeader(i18nString(UIStrings.frames), this._framesHeader, false /* selectable */);
this._frameGroup = this._timelineData.groups[this._timelineData.groups.length - 1];
const style = TimelineUIUtils.markerStyleForFrame();
this._entryTypeByLevel[this._currentLevel] = EntryType.Frame;
for (const frame of this._performanceModel.frames()) {
this._markers.push(
new TimelineFlameChartMarker(frame.startTime, frame.startTime - this._model.minimumRecordTime(), style));
this._appendFrame(frame);
}
++this._currentLevel;
if (!hasFilmStrip) {
return;
}
this._appendHeader('', this._screenshotsHeader, false /* selectable */);
this._entryTypeByLevel[this._currentLevel] = EntryType.Screenshot;
let prevTimestamp: number|undefined;
for (const screenshot of screenshots) {
this._entryData.push(screenshot);
(this._timelineData.entryLevels as number[]).push(this._currentLevel);
(this._timelineData.entryStartTimes as number[]).push(screenshot.timestamp);
if (prevTimestamp) {
(this._timelineData.entryTotalTimes as number[]).push(screenshot.timestamp - prevTimestamp);
}
prevTimestamp = screenshot.timestamp;
}
if (screenshots.length && prevTimestamp !== undefined) {
(this._timelineData.entryTotalTimes as number[]).push(this._model.maximumRecordTime() - prevTimestamp);
}
++this._currentLevel;
}
_entryType(entryIndex: number): EntryType {
return this._entryTypeByLevel[/** @type {!PerfUI.FlameChart.TimelineData} */ (
this._timelineData as PerfUI.FlameChart.TimelineData)
.entryLevels[entryIndex]];
}
prepareHighlightedEntryInfo(entryIndex: number): Element|null {
let time = '';
let title;
let warning;
let nameSpanTimelineInfoTime = 'timeline-info-time';
const type = this._entryType(entryIndex);
if (type === EntryType.Event) {
const event = (this._entryData[entryIndex] as SDK.TracingModel.Event);
const totalTime = event.duration;
const selfTime = event.selfTime;
const eps = 1e-6;
if (typeof totalTime === 'number') {
time = Math.abs(totalTime - selfTime) > eps && selfTime > eps ?
i18nString(
UIStrings.sSelfS,
{PH1: Number.millisToString(totalTime, true), PH2: Number.millisToString(selfTime, true)}) :
Number.millisToString(totalTime, true);
}
if (this._performanceModel && this._performanceModel.timelineModel().isMarkerEvent(event)) {
title = TimelineUIUtils.eventTitle(event);
} else {
title = this.entryTitle(entryIndex);
}
warning = TimelineUIUtils.eventWarning(event);
if (this._model && this._model.isLayoutShiftEvent(event)) {
// TODO: Update this to be dynamic when the trace data supports it.
const occurrences = 1;
time = i18nString(UIStrings.occurrencesS, {PH1: occurrences});
}
if (this._model && this._model.isParseHTMLEvent(event)) {
const startLine = event.args['beginData']['startLine'];
const endLine = event.args['endData'] && event.args['endData']['endLine'];
const url = Bindings.ResourceUtils.displayNameForURL(event.args['beginData']['url']);
const range = (endLine !== -1 || endLine === startLine) ? `${startLine}...${endLine}` : startLine;
title += ` - ${url} [${range}]`;
}
} else if (type === EntryType.Frame) {
const frame = (this._entryData[entryIndex] as TimelineModel.TimelineFrameModel.TimelineFrame);
time = i18nString(
UIStrings.sFfps,
{PH1: Number.preciseMillisToString(frame.duration, 1), PH2: (1000 / frame.duration).toFixed(0)});
if (frame.idle) {
title = i18nString(UIStrings.idleFrame);
} else if (frame.dropped) {
title = i18nString(UIStrings.droppedFrame);
nameSpanTimelineInfoTime = 'timeline-info-warning';
} else {
title = i18nString(UIStrings.frame);
}
if (frame.hasWarnings()) {
warning = document.createElement('span');
warning.textContent = i18nString(UIStrings.longFrame);
}
} else {
return null;
}
const element = document.createElement('div');
const root = UI.Utils.createShadowRootWithCoreStyles(
element,
{cssFile: 'timeline/timelineFlamechartPopover.css', enableLegacyPatching: true, delegatesFocus: undefined});
const contents = root.createChild('div', 'timeline-flamechart-popover');
contents.createChild('span', nameSpanTimelineInfoTime).textContent = time;
contents.createChild('span', 'timeline-info-title').textContent = title;
if (warning) {
warning.classList.add('timeline-info-warning');
contents.appendChild(warning);
}
return element;
}
entryColor(entryIndex: number): string {
function patchColorAndCache<KEY>(cache: Map<KEY, string>, key: KEY, lookupColor: (arg0: KEY) => string): string {
let color = cache.get(key);
if (color) {
return color;
}
const parsedColor = Common.Color.Color.parse(lookupColor(key));
if (!parsedColor) {
throw new Error('Could not parse color from entry');
}
color = parsedColor.setAlpha(0.7).asString(Common.Color.Format.RGBA) || '';
cache.set(key, color);
return color;
}
if (!this._performanceModel || !this._model) {
return '';
}
const entryTypes = EntryType;
const type = this._entryType(entryIndex);
if (type === entryTypes.Event) {
const event = (this._entryData[entryIndex] as SDK.TracingModel.Event);
if (this._model.isGenericTrace()) {
return this._genericTraceEventColor(event);
}
if (this._performanceModel.timelineModel().isMarkerEvent(event)) {
return TimelineUIUtils.markerStyleForEvent(event).color;
}
if (!SDK.TracingModel.TracingModel.isAsyncPhase(event.phase) && this._colorForEvent) {
return this._colorForEvent(event);
}
if (event.hasCategory(TimelineModel.TimelineModel.TimelineModelImpl.Category.Console) ||
event.hasCategory(TimelineModel.TimelineModel.TimelineModelImpl.Category.UserTiming)) {
return this._consoleColorGenerator.colorForID(event.name);
}
if (event.hasCategory(TimelineModel.TimelineModel.TimelineModelImpl.Category.LatencyInfo)) {
const phase = TimelineModel.TimelineIRModel.TimelineIRModel.phaseForEvent(event) ||
TimelineModel.TimelineIRModel.Phases.Uncategorized;
return patchColorAndCache(this._asyncColorByInteractionPhase, phase, TimelineUIUtils.interactionPhaseColor);
}
const category = TimelineUIUtils.eventStyle(event).category;
return patchColorAndCache(this._asyncColorByCategory, category, () => category.color);
}
if (type === entryTypes.Frame) {
return 'white';
}
if (type === entryTypes.InteractionRecord) {
return 'transparent';
}
if (type === entryTypes.ExtensionEvent) {
const event = (this._entryData[entryIndex] as SDK.TracingModel.Event);
return this._extensionColorGenerator.colorForID(event.name);
}
return '';
}
_genericTraceEventColor(event: SDK.TracingModel.Event): string {
const key = event.categoriesString || event.name;
return key ? `hsl(${Platform.StringUtilities.hashCode(key) % 300 + 30}, 40%, 70%)` : '#ccc';
}
_drawFrame(
entryIndex: number, context: CanvasRenderingContext2D, text: string|null, barX: number, barY: number,
barWidth: number, barHeight: number): void {
const hPadding = 1;
const frame = (this._entryData[entryIndex] as TimelineModel.TimelineFrameModel.TimelineFrame);
barX += hPadding;
barWidth -= 2 * hPadding;
context.fillStyle =
frame.idle ? 'white' : frame.dropped ? '#f0b7b1' : (frame.hasWarnings() ? '#fad1d1' : '#d7f0d1');
context.fillRect(barX, barY, barWidth, barHeight);
const frameDurationText = Number.preciseMillisToString(frame.duration, 1);
const textWidth = context.measureText(frameDurationText).width;
if (textWidth <= barWidth) {
context.fillStyle = this.textColor(entryIndex);
context.fillText(frameDurationText, barX + (barWidth - textWidth) / 2, barY + barHeight - 4);
}
}
async _drawScreenshot(
entryIndex: number, context: CanvasRenderingContext2D, barX: number, barY: number, barWidth: number,
barHeight: number): Promise<void> {
const screenshot = (this._entryData[entryIndex] as SDK.FilmStripModel.Frame);
if (!this._screenshotImageCache.has(screenshot)) {
this._screenshotImageCache.set(screenshot, null);
const data = await screenshot.imageDataPromise();
const image = await UI.UIUtils.loadImageFromData(data);
this._screenshotImageCache.set(screenshot, image);
this.dispatchEventToListeners(Events.DataChanged);
return;
}
const image = this._screenshotImageCache.get(screenshot);
if (!image) {
return;
}
const imageX = barX + 1;
const imageY = barY + 1;
const imageHeight = barHeight - 2;
const scale = imageHeight / image.naturalHeight;
const imageWidth = Math.floor(image.naturalWidth * scale);
context.save();
context.beginPath();
context.rect(barX, barY, barWidth, barHeight);
context.clip();
context.drawImage(image, imageX, imageY, imageWidth, imageHeight);
context.strokeStyle = '#ccc';
context.strokeRect(imageX - 0.5, imageY - 0.5, Math.min(barWidth - 1, imageWidth + 1), imageHeight);
context.restore();
}
decorateEntry(
entryIndex: number, context: CanvasRenderingContext2D, text: string|null, barX: number, barY: number,
barWidth: number, barHeight: number, unclippedBarX: number, timeToPixels: number): boolean {
const data = this._entryData[entryIndex];
const type = this._entryType(entryIndex);
const entryTypes = EntryType;
if (type === entryTypes.Frame) {
this._drawFrame(entryIndex, context, text, barX, barY, barWidth, barHeight);
return true;
}
if (type === entryTypes.Screenshot) {
this._drawScreenshot(entryIndex, context, barX, barY, barWidth, barHeight);
return true;
}
if (type === entryTypes.InteractionRecord) {
const color = TimelineUIUtils.interactionPhaseColor((data as TimelineModel.TimelineIRModel.Phases));
context.fillStyle = color;
context.fillRect(barX, barY, barWidth - 1, 2);
context.fillRect(barX, barY - 3, 2, 3);
context.fillRect(barX + barWidth - 3, barY - 3, 2, 3);
return false;
}
if (type === entryTypes.Event) {
const event = (data as SDK.TracingModel.Event);
if (event.hasCategory(TimelineModel.TimelineModel.TimelineModelImpl.Category.LatencyInfo)) {
const timeWaitingForMainThread =
TimelineModel.TimelineModel.TimelineData.forEvent(event).timeWaitingForMainThread;
if (timeWaitingForMainThread) {
context.fillStyle = 'hsla(0, 70%, 60%, 1)';
const width = Math.floor(unclippedBarX - barX + timeWaitingForMainThread * timeToPixels);
context.fillRect(barX, barY + barHeight - 3, width, 2);
}
}
if (TimelineModel.TimelineModel.TimelineData.forEvent(event).warning) {
paintWarningDecoration(barX, barWidth - 1.5);
}
}
function paintWarningDecoration(x: number, width: number): void {
const /** @const */ triangleSize = 8;
context.save();
context.beginPath();
context.rect(x, barY, width, barHeight);
context.clip();
context.beginPath();
context.fillStyle = 'red';
context.moveTo(x + width - triangleSize, barY);
context.lineTo(x + width, barY);
context.lineTo(x + width, barY + triangleSize);
context.fill();
context.restore();
}
return false;
}
forceDecoration(entryIndex: number): boolean {
const entryTypes = EntryType;
const type = this._entryType(entryIndex);
if (type === entryTypes.Frame) {
return true;
}
if (type === entryTypes.Screenshot) {
return true;
}
if (type === entryTypes.Event) {
const event = (this._entryData[entryIndex] as SDK.TracingModel.Event);
return Boolean(TimelineModel.TimelineModel.TimelineData.forEvent(event).warning);
}
return false;
}
appendExtensionEvents(entry: {
title: string,
model: SDK.TracingModel.TracingModel,
}): void {
this._extensionInfo.push(entry);
if (this._timelineData) {
this._innerAppendExtensionEvents(this._extensionInfo.length - 1);
}
}
_innerAppendExtensionEvents(index: number): void {
const entry = this._extensionInfo[index];
const entryType = EntryType.ExtensionEvent;
const allThreads = [...entry.model.sortedProcesses().map(process => process.sortedThreads())].flat();
if (!allThreads.length) {
return;
}
const singleTrack =
allThreads.length === 1 && (!allThreads[0].events().length || !allThreads[0].asyncEvents().length);
if (!singleTrack) {
this._appendHeader(entry.title, this._headerLevel1, false /* selectable */);
}
const style = singleTrack ? this._headerLevel2 : this._headerLevel1;
let threadIndex = 0;
for (const thread of allThreads) {
const title = singleTrack ? entry.title : thread.name() || i18nString(UIStrings.threadS, {PH1: ++threadIndex});
this._appendAsyncEventsGroup(null, title, thread.asyncEvents(), style, entryType, false /* selectable */);
this._appendSyncEvents(null, thread.events(), title, style, entryType, false /* selectable */);
}
}
_appendHeader(title: string, style: PerfUI.FlameChart.GroupStyle, selectable: boolean): PerfUI.FlameChart.Group {
const group =
({startLevel: this._currentLevel, name: title, style: style, selectable: selectable} as
PerfUI.FlameChart.Group);
(this._timelineData as PerfUI.FlameChart.TimelineData).groups.push(group);
return group;
}
_appendEvent(event: SDK.TracingModel.Event, level: number): number {
const index = this._entryData.length;
this._entryData.push(event);
const timelineData = (this._timelineData as PerfUI.FlameChart.TimelineData);
timelineData.entryLevels[index] = level;
timelineData.entryTotalTimes[index] = event.duration || InstantEventVisibleDurationMs;
timelineData.entryStartTimes[index] = event.startTime;
indexForEvent.set(event, index);
return index;
}
_appendAsyncEvent(asyncEvent: SDK.TracingModel.AsyncEvent, level: number): void {
if (SDK.TracingModel.TracingModel.isNestableAsyncPhase(asyncEvent.phase)) {
// FIXME: also add steps once we support event nesting in the FlameChart.
this._appendEvent(asyncEvent, level);
return;
}
const steps = asyncEvent.steps;
// If we have past steps, put the end event for each range rather than start one.
const eventOffset = steps.length > 1 && steps[1].phase === SDK.TracingModel.Phase.AsyncStepPast ? 1 : 0;
for (let i = 0; i < steps.length - 1; ++i) {
const index = this._entryData.length;
this._entryData.push(steps[i + eventOffset]);
const startTime = steps[i].startTime;
const timelineData = (this._timelineData as PerfUI.FlameChart.TimelineData);
timelineData.entryLevels[index] = level;
timelineData.entryTotalTimes[index] = steps[i + 1].startTime - startTime;
timelineData.entryStartTimes[index] = startTime;
}
}
_appendFrame(frame: TimelineModel.TimelineFrameModel.TimelineFrame): void {
const index = this._entryData.length;
this._entryData.push(frame);
this._entryIndexToTitle[index] = Number.millisToString(frame.duration, true);
if (!this._timelineData) {
return;
}
this._timelineData.entryLevels[index] = this._currentLevel;
this._timelineData.entryTotalTimes[index] = frame.duration;
this._timelineData.entryStartTimes[index] = frame.startTime;
}
createSelection(entryIndex: number): TimelineSelection|null {
const type = this._entryType(entryIndex);
let timelineSelection: TimelineSelection|null = null;
if (type === EntryType.Event) {
timelineSelection = TimelineSelection.fromTraceEvent((this._entryData[entryIndex] as SDK.TracingModel.Event));
} else if (type === EntryType.Frame) {
timelineSelection =
TimelineSelection.fromFrame((this._entryData[entryIndex] as TimelineModel.TimelineFrameModel.TimelineFrame));
}
if (timelineSelection) {
this._lastSelection = new Selection(timelineSelection, entryIndex);
}
return timelineSelection;
}
formatValue(value: number, precision?: number): string {
return Number.preciseMillisToString(value, precision);
}
canJumpToEntry(_entryIndex: number): boolean {
return false;
}
entryIndexForSelection(selection: TimelineSelection|null): number {
if (!selection || selection.type() === TimelineSelection.Type.Range) {
return -1;
}
if (this._lastSelection && this._lastSelection.timelineSelection.object() === selection.object()) {
return this._lastSelection.entryIndex;
}
const index = this._entryData.indexOf(
(selection.object() as SDK.TracingModel.Event | TimelineModel.TimelineIRModel.Phases |
TimelineModel.TimelineFrameModel.TimelineFrame));
if (index !== -1) {
this._lastSelection = new Selection(selection, index);
}
return index;
}
buildFlowForInitiator(entryIndex: number): boolean {
if (this._lastInitiatorEntry === entryIndex) {
return false;
}
this._lastInitiatorEntry = entryIndex;
let event = this.eventByIndex(entryIndex);
const td = this._timelineData;
if (!td) {
return false;
}
td.flowStartTimes = [];
td.flowStartLevels = [];
td.flowEndTimes = [];
td.flowEndLevels = [];
while (event) {
// Find the closest ancestor with an initiator.
let initiator;
for (; event; event = this._eventParent(event)) {
initiator = TimelineModel.TimelineModel.TimelineData.forEvent(event).initiator();
if (initiator) {
break;
}
}
if (!initiator || !event) {
break;
}
const eventIndex = (indexForEvent.get(event) as number);
const initiatorIndex = (indexForEvent.get(initiator) as number);
td.flowStartTimes.push(initiator.endTime || initiator.startTime);
td.flowStartLevels.push(td.entryLevels[initiatorIndex]);
td.flowEndTimes.push(event.startTime);
td.flowEndLevels.push(td.entryLevels[eventIndex]);
event = initiator;
}
return true;
}
_eventParent(event: SDK.TracingModel.Event): SDK.TracingModel.Event|null {
const eventIndex = indexForEvent.get(event);
if (eventIndex === undefined) {
return null;
}
return this._entryParent[eventIndex] || null;
}
eventByIndex(entryIndex: number): SDK.TracingModel.Event|null {
return entryIndex >= 0 && this._entryType(entryIndex) === EntryType.Event ?
this._entryData[entryIndex] as SDK.TracingModel.Event :
null;
}
setEventColorMapping(colorForEvent: (arg0: SDK.TracingModel.Event) => string): void {
this._colorForEvent = colorForEvent;
}
}
export const InstantEventVisibleDurationMs = 0.001;
const eventToDisallowRoot = new WeakMap<SDK.TracingModel.Event, boolean>();
const indexForEvent = new WeakMap<SDK.TracingModel.Event, number>();
// TODO(crbug.com/1167717): Make this a const enum again
// eslint-disable-next-line rulesdir/const_enum
export enum Events {
DataChanged = 'DataChanged',
}
// TODO(crbug.com/1167717): Make this a const enum again
// eslint-disable-next-line rulesdir/const_enum
export enum EntryType {
Frame = 'Frame',
Event = 'Event',
InteractionRecord = 'InteractionRecord',
ExtensionEvent = 'ExtensionEvent',
Screenshot = 'Screenshot',
}