chrome-devtools-frontend
Version:
Chrome DevTools UI
639 lines (563 loc) • 25.4 kB
text/typescript
// Copyright 2017 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.
/* eslint-disable rulesdir/no-imperative-dom-api */
import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Trace from '../../models/trace/trace.js';
import * as TraceBounds from '../../services/trace_bounds/trace_bounds.js';
import * as Components from '../../ui/legacy/components/utils/utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import * as TimelineComponents from './components/components.js';
import {EventsTimelineTreeView} from './EventsTimelineTreeView.js';
import {Tracker} from './FreshRecording.js';
import {targetForEvent} from './TargetForEvent.js';
import {ThirdPartyTreeViewWidget} from './ThirdPartyTreeView.js';
import {TimelineLayersView} from './TimelineLayersView.js';
import {TimelinePaintProfilerView} from './TimelinePaintProfilerView.js';
import type {TimelineModeViewDelegate} from './TimelinePanel.js';
import {
selectionFromRangeMilliSeconds,
selectionIsEvent,
selectionIsRange,
type TimelineSelection,
} from './TimelineSelection.js';
import {TimelineSelectorStatsView} from './TimelineSelectorStatsView.js';
import {
AggregatedTimelineTreeView,
BottomUpTimelineTreeView,
CallTreeTimelineTreeView,
TimelineStackView,
TimelineTreeView
} from './TimelineTreeView.js';
import {TimelineUIUtils} from './TimelineUIUtils.js';
import {TracingFrameLayerTree} from './TracingLayerTree.js';
import * as Utils from './utils/utils.js';
const UIStrings = {
/**
*@description Text for the summary view
*/
summary: 'Summary',
/**
*@description Text in Timeline Details View of the Performance panel
*/
bottomup: 'Bottom-up',
/**
*@description Text in Timeline Details View of the Performance panel
*/
callTree: 'Call tree',
/**
*@description Text in Timeline Details View of the Performance panel
*/
eventLog: 'Event log',
/**
*@description Title of the paint profiler, old name of the performance pane
*/
paintProfiler: 'Paint profiler',
/**
*@description Title of the Layers tool
*/
layers: 'Layers',
/**
*@description Title of the selector stats tab
*/
selectorStats: 'Selector stats',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/timeline/TimelineDetailsView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export class TimelineDetailsPane extends
Common.ObjectWrapper.eventMixin<TimelineTreeView.EventTypes, typeof UI.Widget.VBox>(UI.Widget.VBox) {
private readonly detailsLinkifier: Components.Linkifier.Linkifier;
private tabbedPane: UI.TabbedPane.TabbedPane;
private readonly defaultDetailsWidget: UI.Widget.VBox;
private defaultDetailsContentWidget: UI.Widget.VBox;
private rangeDetailViews: Map<string, TimelineTreeView>;
#selectedEvents?: Trace.Types.Events.Event[]|null;
private lazyPaintProfilerView?: TimelinePaintProfilerView|null;
private lazyLayersView?: TimelineLayersView|null;
private preferredTabId?: string;
private selection?: TimelineSelection|null;
private updateContentsScheduled: boolean;
private lazySelectorStatsView: TimelineSelectorStatsView|null;
#parsedTrace: Trace.Handlers.Types.ParsedTrace|null = null;
#traceInsightsSets: Trace.Insights.Types.TraceInsightSets|null = null;
#eventToRelatedInsightsMap: TimelineComponents.RelatedInsightChips.EventToRelatedInsightsMap|null = null;
#filmStrip: Trace.Extras.FilmStrip.Data|null = null;
#networkRequestDetails: TimelineComponents.NetworkRequestDetails.NetworkRequestDetails;
#layoutShiftDetails: TimelineComponents.LayoutShiftDetails.LayoutShiftDetails;
#onTraceBoundsChangeBound = this.#onTraceBoundsChange.bind(this);
#relatedInsightChips = new TimelineComponents.RelatedInsightChips.RelatedInsightChips();
#thirdPartyTree = new ThirdPartyTreeViewWidget();
#entityMapper: Utils.EntityMapper.EntityMapper|null = null;
constructor(delegate: TimelineModeViewDelegate) {
super();
this.element.classList.add('timeline-details');
this.detailsLinkifier = new Components.Linkifier.Linkifier();
this.tabbedPane = new UI.TabbedPane.TabbedPane();
this.tabbedPane.show(this.element);
this.tabbedPane.headerElement().setAttribute(
'jslog',
`${VisualLogging.toolbar('sidebar').track({keydown: 'ArrowUp|ArrowLeft|ArrowDown|ArrowRight|Enter|Space'})}`);
this.defaultDetailsWidget = new UI.Widget.VBox();
this.defaultDetailsWidget.element.classList.add('timeline-details-view');
this.defaultDetailsWidget.element.setAttribute('jslog', `${VisualLogging.pane('details').track({resize: true})}`);
this.defaultDetailsContentWidget = this.#createContentWidget();
this.appendTab(Tab.Details, i18nString(UIStrings.summary), this.defaultDetailsWidget);
this.setPreferredTab(Tab.Details);
this.rangeDetailViews = new Map();
this.updateContentsScheduled = false;
const bottomUpView = new BottomUpTimelineTreeView();
this.appendTab(Tab.BottomUp, i18nString(UIStrings.bottomup), bottomUpView);
this.rangeDetailViews.set(Tab.BottomUp, bottomUpView);
const callTreeView = new CallTreeTimelineTreeView();
this.appendTab(Tab.CallTree, i18nString(UIStrings.callTree), callTreeView);
this.rangeDetailViews.set(Tab.CallTree, callTreeView);
const eventsView = new EventsTimelineTreeView(delegate);
this.appendTab(Tab.EventLog, i18nString(UIStrings.eventLog), eventsView);
this.rangeDetailViews.set(Tab.EventLog, eventsView);
// Listeners for hover dimming
this.rangeDetailViews.values().forEach(view => {
view.addEventListener(
TimelineTreeView.Events.TREE_ROW_HOVERED,
node => this.dispatchEventToListeners(TimelineTreeView.Events.TREE_ROW_HOVERED, node.data));
view.addEventListener(TimelineTreeView.Events.TREE_ROW_CLICKED, node => {
// Re-dispatch to reach the tree row dimmer.
this.dispatchEventToListeners(TimelineTreeView.Events.TREE_ROW_CLICKED, node.data);
});
// If there's a heaviest stack sidebar view, also listen to hover within it.
if (view instanceof AggregatedTimelineTreeView) {
view.stackView.addEventListener(
TimelineStackView.Events.TREE_ROW_HOVERED,
node => this.dispatchEventToListeners(TimelineTreeView.Events.TREE_ROW_HOVERED, {node: node.data}));
}
});
this.#thirdPartyTree.addEventListener(TimelineTreeView.Events.TREE_ROW_HOVERED, node => {
// Re-dispatch through 3P event to get 3P dimmer.
this.dispatchEventToListeners(
TimelineTreeView.Events.TREE_ROW_HOVERED, {node: node.data.node, events: node.data.events ?? undefined});
});
this.#thirdPartyTree.addEventListener(TimelineTreeView.Events.BOTTOM_UP_BUTTON_CLICKED, node => {
this.selectTab(Tab.BottomUp, node.data, AggregatedTimelineTreeView.GroupBy.ThirdParties);
});
this.#thirdPartyTree.addEventListener(TimelineTreeView.Events.TREE_ROW_CLICKED, node => {
// Re-dispatch through 3P event to get 3P dimmer.
this.dispatchEventToListeners(
TimelineTreeView.Events.TREE_ROW_CLICKED, {node: node.data.node, events: node.data.events ?? undefined});
});
this.#networkRequestDetails =
new TimelineComponents.NetworkRequestDetails.NetworkRequestDetails(this.detailsLinkifier);
this.#layoutShiftDetails = new TimelineComponents.LayoutShiftDetails.LayoutShiftDetails();
this.tabbedPane.addEventListener(UI.TabbedPane.Events.TabSelected, this.tabSelected, this);
TraceBounds.TraceBounds.onChange(this.#onTraceBoundsChangeBound);
this.lazySelectorStatsView = null;
}
/**
* This selects a given tabbedPane tab.
* Additionally, if provided a node, we open that node and
* if a groupBySetting is included, we groupBy.
*/
selectTab(tabName: Tab, node: Trace.Extras.TraceTree.Node|null, groupBySetting?: AggregatedTimelineTreeView.GroupBy):
void {
this.tabbedPane.selectTab(tabName, true, true);
/**
* For a11y, ensure that the header is focused.
*/
this.tabbedPane.focusSelectedTabHeader();
// We currently only support selecting Details and BottomUp via the 3P insight.
switch (tabName) {
case Tab.CallTree:
case Tab.EventLog:
case Tab.PaintProfiler:
case Tab.LayerViewer:
case Tab.SelectorStats: {
break;
}
case Tab.Details: {
this.updateContentsFromWindow();
break;
}
case Tab.BottomUp: {
if (!(this.tabbedPane.visibleView instanceof BottomUpTimelineTreeView)) {
return;
}
// Set grouping if necessary.
const bottomUp = this.tabbedPane.visibleView;
if (groupBySetting) {
bottomUp.setGroupBySetting(groupBySetting);
bottomUp.refreshTree();
}
if (!node) {
return;
}
// Look for the equivalent GroupNode in the bottomUp tree using the node's reference `event`.
// Conceivably, we could match using the group ID instead.
const treeNode = bottomUp.eventToTreeNode.get(node.event);
if (!treeNode) {
return;
}
bottomUp.selectProfileNode(treeNode, true);
// Reveal/expand the bottom up tree grid node.
const gridNode = bottomUp.dataGridNodeForTreeNode(treeNode);
if (gridNode) {
gridNode.expand();
}
break;
}
default: {
Platform.assertNever(tabName, `Unknown Tab: ${tabName}. Add new case to switch.`);
}
}
}
#createContentWidget(): UI.Widget.VBox {
const defaultDetailsContentWidget = new UI.Widget.VBox();
defaultDetailsContentWidget.element.classList.add('timeline-details-view-body');
defaultDetailsContentWidget.show(this.defaultDetailsWidget.element);
return defaultDetailsContentWidget;
}
private selectorStatsView(): TimelineSelectorStatsView {
if (this.lazySelectorStatsView) {
return this.lazySelectorStatsView;
}
this.lazySelectorStatsView = new TimelineSelectorStatsView(
this.#parsedTrace,
);
return this.lazySelectorStatsView;
}
getDetailsContentElementForTest(): HTMLElement {
return this.defaultDetailsContentWidget.element;
}
revealEventInTreeView(event: Trace.Types.Events.Event|null): void {
if (this.tabbedPane.visibleView instanceof TimelineTreeView) {
this.tabbedPane.visibleView.highlightEventInTree(event);
}
}
async #onTraceBoundsChange(event: TraceBounds.TraceBounds.StateChangedEvent): Promise<void> {
if (event.updateType === 'MINIMAP_BOUNDS') {
// If new minimap bounds are set, we might need to update the selected entry summary because
// the links to other entries (ex. initiator) might be outside of the new breadcrumb.
if (this.selection) {
await this.setSelection(this.selection);
}
}
if (event.updateType === 'RESET' || event.updateType === 'VISIBLE_WINDOW') {
// If the update type was a changing of the minimap bounds, we do not
// need to redraw.
if (!this.selection) {
this.scheduleUpdateContentsFromWindow();
}
}
}
async setModel(data: {
parsedTrace: Trace.Handlers.Types.ParsedTrace|null,
selectedEvents: Trace.Types.Events.Event[]|null,
traceInsightsSets: Trace.Insights.Types.TraceInsightSets|null,
eventToRelatedInsightsMap: TimelineComponents.RelatedInsightChips.EventToRelatedInsightsMap|null,
entityMapper: Utils.EntityMapper.EntityMapper|null,
}): Promise<void> {
if (this.#parsedTrace !== data.parsedTrace) {
// Clear the selector stats view, so the next time the user views it we
// reconstruct it with the new trace data.
this.lazySelectorStatsView = null;
this.#parsedTrace = data.parsedTrace;
}
if (data.parsedTrace) {
this.#filmStrip = Trace.Extras.FilmStrip.fromParsedTrace(data.parsedTrace);
this.#entityMapper = new Utils.EntityMapper.EntityMapper(data.parsedTrace);
}
this.#selectedEvents = data.selectedEvents;
this.#traceInsightsSets = data.traceInsightsSets;
this.#eventToRelatedInsightsMap = data.eventToRelatedInsightsMap;
if (data.eventToRelatedInsightsMap) {
this.#relatedInsightChips.eventToRelatedInsightsMap = data.eventToRelatedInsightsMap;
}
this.tabbedPane.closeTabs([Tab.PaintProfiler, Tab.LayerViewer], false);
for (const view of this.rangeDetailViews.values()) {
view.setModelWithEvents(data.selectedEvents, data.parsedTrace, data.entityMapper);
}
// Set the 3p tree model.
this.#thirdPartyTree.setModelWithEvents(data.selectedEvents, data.parsedTrace, data.entityMapper);
this.lazyPaintProfilerView = null;
this.lazyLayersView = null;
await this.setSelection(null);
}
private setSummaryContent(node: Node): void {
const allTabs = this.tabbedPane.otherTabs(Tab.Details);
for (let i = 0; i < allTabs.length; ++i) {
if (!this.rangeDetailViews.has(allTabs[i])) {
this.tabbedPane.closeTab(allTabs[i]);
}
}
// Append relatedChips inside of the node being shown.
const chipParent = (node instanceof Element && node.shadowRoot || node);
chipParent.appendChild(this.#relatedInsightChips);
this.defaultDetailsContentWidget.detach();
this.defaultDetailsContentWidget = this.#createContentWidget();
this.defaultDetailsContentWidget.contentElement.append(node);
}
private updateContents(): void {
const traceBoundsState = TraceBounds.TraceBounds.BoundsManager.instance().state();
if (!traceBoundsState) {
return;
}
const visibleWindow = traceBoundsState.milli.timelineTraceWindow;
// Update the view that we currently have selected.
const view = this.rangeDetailViews.get(this.tabbedPane.selectedTabId || '');
if (view) {
view.updateContents(this.selection || selectionFromRangeMilliSeconds(visibleWindow.min, visibleWindow.max));
}
}
private appendTab(id: string, tabTitle: string, view: UI.Widget.Widget, isCloseable?: boolean): void {
this.tabbedPane.appendTab(id, tabTitle, view, undefined, undefined, isCloseable);
if (this.preferredTabId !== this.tabbedPane.selectedTabId) {
this.tabbedPane.selectTab(id);
}
}
headerElement(): Element {
return this.tabbedPane.headerElement();
}
setPreferredTab(tabId: string): void {
this.preferredTabId = tabId;
}
/**
* This forces a recalculation and rerendering of the timings
* breakdown of a track.
* User actions like zooming or scrolling can trigger many updates in
* short time windows, so we debounce the calls in those cases. Single
* sporadic calls (like selecting a new track) don't need to be
* debounced. The forceImmediateUpdate param configures the debouncing
* behaviour.
*/
private scheduleUpdateContentsFromWindow(forceImmediateUpdate = false): void {
if (!this.#parsedTrace) {
this.setSummaryContent(UI.Fragment.html`<div/>`);
return;
}
if (forceImmediateUpdate) {
this.updateContentsFromWindow();
return;
}
// Debounce this update as it's not critical.
if (!this.updateContentsScheduled) {
this.updateContentsScheduled = true;
setTimeout(() => {
if (!this.updateContentsScheduled) {
return;
}
this.updateContentsScheduled = false;
this.updateContentsFromWindow();
}, 100);
}
}
private updateContentsFromWindow(): void {
const traceBoundsState = TraceBounds.TraceBounds.BoundsManager.instance().state();
if (!traceBoundsState) {
return;
}
const visibleWindow = traceBoundsState.milli.timelineTraceWindow;
this.updateSelectedRangeStats(visibleWindow.min, visibleWindow.max);
this.updateContents();
}
#getFilmStripFrame(frame: Trace.Types.Events.LegacyTimelineFrame): Trace.Extras.FilmStrip.Frame|null {
if (!this.#filmStrip) {
return null;
}
const screenshotTime = (frame.idle ? frame.startTime : frame.endTime);
const filmStripFrame = Trace.Extras.FilmStrip.frameClosestToTimestamp(this.#filmStrip, screenshotTime);
if (!filmStripFrame) {
return null;
}
const frameTimeMilliSeconds = Trace.Helpers.Timing.microToMilli(filmStripFrame.screenshotEvent.ts);
const frameEndTimeMilliSeconds = Trace.Helpers.Timing.microToMilli(frame.endTime);
return frameTimeMilliSeconds - frameEndTimeMilliSeconds < 10 ? filmStripFrame : null;
}
#setSelectionForTimelineFrame(frame: Trace.Types.Events.LegacyTimelineFrame): void {
const matchedFilmStripFrame = this.#getFilmStripFrame(frame);
this.setSummaryContent(
TimelineUIUtils.generateDetailsContentForFrame(frame, this.#filmStrip, matchedFilmStripFrame));
const target = SDK.TargetManager.TargetManager.instance().rootTarget();
if (frame.layerTree && target) {
const layerTreeForFrame = new TracingFrameLayerTree(target, frame.layerTree);
const layersView = this.layersView();
layersView.showLayerTree(layerTreeForFrame);
if (!this.tabbedPane.hasTab(Tab.LayerViewer)) {
this.appendTab(Tab.LayerViewer, i18nString(UIStrings.layers), layersView);
}
}
}
async #setSelectionForNetworkEvent(networkRequest: Trace.Types.Events.SyntheticNetworkRequest): Promise<void> {
if (!this.#parsedTrace) {
return;
}
const maybeTarget = targetForEvent(this.#parsedTrace, networkRequest);
await this.#networkRequestDetails.setData(this.#parsedTrace, networkRequest, maybeTarget, this.#entityMapper);
this.#relatedInsightChips.activeEvent = networkRequest;
if (this.#eventToRelatedInsightsMap) {
this.#relatedInsightChips.eventToRelatedInsightsMap = this.#eventToRelatedInsightsMap;
}
this.setSummaryContent(this.#networkRequestDetails);
}
async #setSelectionForTraceEvent(event: Trace.Types.Events.Event): Promise<void> {
if (!this.#parsedTrace) {
return;
}
this.#relatedInsightChips.activeEvent = event;
if (this.#eventToRelatedInsightsMap) {
this.#relatedInsightChips.eventToRelatedInsightsMap = this.#eventToRelatedInsightsMap;
}
// Special case: if the user selects a layout shift or a layout shift cluster,
// render the new layout shift details component.
if (Trace.Types.Events.isSyntheticLayoutShift(event) || Trace.Types.Events.isSyntheticLayoutShiftCluster(event)) {
const isFreshRecording = Boolean(this.#parsedTrace && Tracker.instance().recordingIsFresh(this.#parsedTrace));
this.#layoutShiftDetails.setData(event, this.#traceInsightsSets, this.#parsedTrace, isFreshRecording);
this.setSummaryContent(this.#layoutShiftDetails);
return;
}
// Otherwise, build the generic trace event details UI.
const traceEventDetails = await TimelineUIUtils.buildTraceEventDetails(
this.#parsedTrace, event, this.detailsLinkifier, true, this.#entityMapper);
this.appendDetailsTabsForTraceEventAndShowDetails(event, traceEventDetails);
}
async setSelection(selection: TimelineSelection|null): Promise<void> {
if (!this.#parsedTrace) {
// You can't make a selection if we have no trace data.
return;
}
this.detailsLinkifier.reset();
this.selection = selection;
this.#relatedInsightChips.activeEvent = null;
if (!this.selection) {
// Update instantly using forceImmediateUpdate, since we are only
// making a single call and don't need to debounce.
this.scheduleUpdateContentsFromWindow(/* forceImmediateUpdate */ true);
return;
}
if (selectionIsEvent(selection)) {
// Cancel any pending debounced range stats update
this.updateContentsScheduled = false;
if (Trace.Types.Events.isSyntheticNetworkRequest(selection.event)) {
await this.#setSelectionForNetworkEvent(selection.event);
} else if (Trace.Types.Events.isLegacyTimelineFrame(selection.event)) {
this.#setSelectionForTimelineFrame(selection.event);
} else {
await this.#setSelectionForTraceEvent(selection.event);
}
} else if (selectionIsRange(selection)) {
const timings = Trace.Helpers.Timing.traceWindowMicroSecondsToMilliSeconds(selection.bounds);
this.updateSelectedRangeStats(timings.min, timings.max);
}
this.updateContents();
}
private tabSelected(event: Common.EventTarget.EventTargetEvent<UI.TabbedPane.EventData>): void {
if (!event.data.isUserGesture) {
return;
}
this.setPreferredTab(event.data.tabId);
this.updateContents();
}
private layersView(): TimelineLayersView {
if (this.lazyLayersView) {
return this.lazyLayersView;
}
this.lazyLayersView = new TimelineLayersView(this.showSnapshotInPaintProfiler.bind(this));
return this.lazyLayersView;
}
private paintProfilerView(): TimelinePaintProfilerView|null {
if (this.lazyPaintProfilerView) {
return this.lazyPaintProfilerView;
}
if (!this.#parsedTrace) {
return null;
}
this.lazyPaintProfilerView = new TimelinePaintProfilerView(this.#parsedTrace);
return this.lazyPaintProfilerView;
}
private showSnapshotInPaintProfiler(snapshot: SDK.PaintProfiler.PaintProfilerSnapshot): void {
const paintProfilerView = this.paintProfilerView();
if (!paintProfilerView) {
return;
}
paintProfilerView.setSnapshot(snapshot);
if (!this.tabbedPane.hasTab(Tab.PaintProfiler)) {
this.appendTab(Tab.PaintProfiler, i18nString(UIStrings.paintProfiler), paintProfilerView, true);
}
this.tabbedPane.selectTab(Tab.PaintProfiler, true);
}
private showSelectorStatsForIndividualEvent(event: Trace.Types.Events.UpdateLayoutTree): void {
this.showAggregatedSelectorStats([event]);
}
private showAggregatedSelectorStats(events: Trace.Types.Events.UpdateLayoutTree[]): void {
const selectorStatsView = this.selectorStatsView();
selectorStatsView.setAggregatedEvents(events);
if (!this.tabbedPane.hasTab(Tab.SelectorStats)) {
this.appendTab(Tab.SelectorStats, i18nString(UIStrings.selectorStats), selectorStatsView);
}
}
private appendDetailsTabsForTraceEventAndShowDetails(event: Trace.Types.Events.Event, content: Node): void {
this.setSummaryContent(content);
if (Trace.Types.Events.isPaint(event) || Trace.Types.Events.isRasterTask(event)) {
this.showEventInPaintProfiler(event);
}
if (Trace.Types.Events.isUpdateLayoutTree(event)) {
this.showSelectorStatsForIndividualEvent(event);
}
}
private showEventInPaintProfiler(event: Trace.Types.Events.Event): void {
const paintProfilerModel =
SDK.TargetManager.TargetManager.instance().models(SDK.PaintProfiler.PaintProfilerModel)[0];
if (!paintProfilerModel) {
return;
}
const paintProfilerView = this.paintProfilerView();
if (!paintProfilerView) {
return;
}
const hasProfileData = paintProfilerView.setEvent(paintProfilerModel, event);
if (!hasProfileData) {
return;
}
if (this.tabbedPane.hasTab(Tab.PaintProfiler)) {
return;
}
this.appendTab(Tab.PaintProfiler, i18nString(UIStrings.paintProfiler), paintProfilerView);
}
private updateSelectedRangeStats(startTime: Trace.Types.Timing.Milli, endTime: Trace.Types.Timing.Milli): void {
if (!this.#selectedEvents || !this.#parsedTrace || !this.#entityMapper) {
return;
}
const minBoundsMilli = Trace.Helpers.Timing.traceWindowMilliSeconds(this.#parsedTrace.Meta.traceBounds).min;
const aggregatedStats = TimelineUIUtils.statsForTimeRange(this.#selectedEvents, startTime, endTime);
const startOffset = startTime - minBoundsMilli;
const endOffset = endTime - minBoundsMilli;
const summaryDetailElem = TimelineUIUtils.generateSummaryDetails(
aggregatedStats, startOffset, endOffset, this.#selectedEvents, this.#thirdPartyTree);
this.#thirdPartyTree.updateContents(this.selection || selectionFromRangeMilliSeconds(startTime, endTime));
this.setSummaryContent(summaryDetailElem);
// Find all recalculate style events data from range
const isSelectorStatsEnabled =
Common.Settings.Settings.instance().createSetting('timeline-capture-selector-stats', false).get();
if (this.#selectedEvents && isSelectorStatsEnabled) {
const eventsInRange = Trace.Helpers.Trace.findUpdateLayoutTreeEvents(
this.#selectedEvents,
Trace.Helpers.Timing.milliToMicro(startTime),
Trace.Helpers.Timing.milliToMicro(endTime),
);
if (eventsInRange.length > 0) {
this.showAggregatedSelectorStats(eventsInRange);
}
}
}
}
export enum Tab {
/* eslint-disable @typescript-eslint/naming-convention -- Used by web_tests. */
Details = 'details',
EventLog = 'event-log',
CallTree = 'call-tree',
BottomUp = 'bottom-up',
PaintProfiler = 'paint-profiler',
LayerViewer = 'layer-viewer',
SelectorStats = 'selector-stats',
/* eslint-enable @typescript-eslint/naming-convention */
}