UNPKG

chrome-devtools-frontend

Version:
496 lines (436 loc) • 20.8 kB
// Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Common from '../../core/common/common.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 TimelineModel from '../../models/timeline_model/timeline_model.js'; import * as TraceEngine from '../../models/trace/trace.js'; import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js'; import * as UI from '../../ui/legacy/legacy.js'; import {CountersGraph} from './CountersGraph.js'; import {Events as PerformanceModelEvents, type PerformanceModel, type WindowChangedEvent} from './PerformanceModel.js'; import {TimelineDetailsView} from './TimelineDetailsView.js'; import {TimelineRegExp} from './TimelineFilters.js'; import { Events as TimelineFlameChartDataProviderEvents, TimelineFlameChartDataProvider, } from './TimelineFlameChartDataProvider.js'; import {TimelineFlameChartNetworkDataProvider} from './TimelineFlameChartNetworkDataProvider.js'; import {type TimelineModeViewDelegate} from './TimelinePanel.js'; import {TimelineSelection} from './TimelineSelection.js'; import {AggregatedTimelineTreeView} from './TimelineTreeView.js'; import {TimelineUIUtils, type TimelineMarkerStyle} from './TimelineUIUtils.js'; const UIStrings = { /** *@description Text in Timeline Flame Chart View of the Performance panel *@example {Frame} PH1 *@example {10ms} PH2 */ sAtS: '{PH1} at {PH2}', }; const str_ = i18n.i18n.registerUIStrings('panels/timeline/TimelineFlameChartView.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export class TimelineFlameChartView extends UI.Widget.VBox implements PerfUI.FlameChart.FlameChartDelegate, UI.SearchableView.Searchable { private readonly delegate: TimelineModeViewDelegate; private model: PerformanceModel|null; private searchResults!: number[]|undefined; private eventListeners: Common.EventTarget.EventDescriptor[]; // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // eslint-disable-next-line @typescript-eslint/no-explicit-any private readonly showMemoryGraphSetting: Common.Settings.Setting<any>; private readonly networkSplitWidget: UI.SplitWidget.SplitWidget; private mainDataProvider: TimelineFlameChartDataProvider; private readonly mainFlameChart: PerfUI.FlameChart.FlameChart; // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // eslint-disable-next-line @typescript-eslint/no-explicit-any private readonly networkFlameChartGroupExpansionSetting: Common.Settings.Setting<any>; private networkDataProvider: TimelineFlameChartNetworkDataProvider; private readonly networkFlameChart: PerfUI.FlameChart.FlameChart; private readonly networkPane: UI.Widget.VBox; private readonly splitResizer: HTMLElement; private readonly chartSplitWidget: UI.SplitWidget.SplitWidget; private readonly countersView: CountersGraph; private readonly detailsSplitWidget: UI.SplitWidget.SplitWidget; private readonly detailsView: TimelineDetailsView; private readonly onMainEntrySelected: (event: Common.EventTarget.EventTargetEvent<number>) => void; private readonly onNetworkEntrySelected: (event: Common.EventTarget.EventTargetEvent<number>) => void; private readonly boundRefresh: () => void; #selectedEvents: SDK.TracingModel.CompatibleTraceEvent[]|null; // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // eslint-disable-next-line @typescript-eslint/no-explicit-any private readonly groupBySetting: Common.Settings.Setting<any>; private searchableView!: UI.SearchableView.SearchableView; private needsResizeToPreferredHeights?: boolean; private selectedSearchResult?: number; private searchRegex?: RegExp; #traceEngineData: TraceEngine.TraceModel.PartialTraceParseDataDuringMigration|null; constructor(delegate: TimelineModeViewDelegate) { super(); this.element.classList.add('timeline-flamechart'); this.delegate = delegate; this.model = null; this.eventListeners = []; this.#traceEngineData = null; this.showMemoryGraphSetting = Common.Settings.Settings.instance().createSetting('timelineShowMemory', false); // Create main and network flamecharts. this.networkSplitWidget = new UI.SplitWidget.SplitWidget(false, false, 'timelineFlamechartMainView', 150); // Ensure that the network panel & resizer appears above the main thread. this.networkSplitWidget.sidebarElement().style.zIndex = '120'; const mainViewGroupExpansionSetting = Common.Settings.Settings.instance().createSetting('timelineFlamechartMainViewGroupExpansion', {}); this.mainDataProvider = new TimelineFlameChartDataProvider(); this.mainDataProvider.addEventListener( TimelineFlameChartDataProviderEvents.DataChanged, () => this.mainFlameChart.scheduleUpdate()); this.mainFlameChart = new PerfUI.FlameChart.FlameChart(this.mainDataProvider, this, mainViewGroupExpansionSetting); this.mainFlameChart.alwaysShowVerticalScroll(); this.mainFlameChart.enableRuler(false); this.networkFlameChartGroupExpansionSetting = Common.Settings.Settings.instance().createSetting('timelineFlamechartNetworkViewGroupExpansion', {}); this.networkDataProvider = new TimelineFlameChartNetworkDataProvider(); this.networkFlameChart = new PerfUI.FlameChart.FlameChart(this.networkDataProvider, this, this.networkFlameChartGroupExpansionSetting); this.networkFlameChart.alwaysShowVerticalScroll(); this.networkPane = new UI.Widget.VBox(); this.networkPane.setMinimumSize(23, 23); this.networkFlameChart.show(this.networkPane.element); this.splitResizer = this.networkPane.element.createChild('div', 'timeline-flamechart-resizer'); this.networkSplitWidget.hideDefaultResizer(true); this.networkSplitWidget.installResizer(this.splitResizer); this.networkSplitWidget.setMainWidget(this.mainFlameChart); this.networkSplitWidget.setSidebarWidget(this.networkPane); // Create counters chart splitter. this.chartSplitWidget = new UI.SplitWidget.SplitWidget(false, true, 'timelineCountersSplitViewState'); this.countersView = new CountersGraph(this.delegate); this.chartSplitWidget.setMainWidget(this.networkSplitWidget); this.chartSplitWidget.setSidebarWidget(this.countersView); this.chartSplitWidget.hideDefaultResizer(); this.chartSplitWidget.installResizer((this.countersView.resizerElement() as Element)); this.updateCountersGraphToggle(); // Create top level properties splitter. this.detailsSplitWidget = new UI.SplitWidget.SplitWidget(false, true, 'timelinePanelDetailsSplitViewState'); this.detailsSplitWidget.element.classList.add('timeline-details-split'); this.detailsView = new TimelineDetailsView(delegate); this.detailsSplitWidget.installResizer(this.detailsView.headerElement()); this.detailsSplitWidget.setMainWidget(this.chartSplitWidget); this.detailsSplitWidget.setSidebarWidget(this.detailsView); this.detailsSplitWidget.show(this.element); this.onMainEntrySelected = this.onEntrySelected.bind(this, this.mainDataProvider); this.onNetworkEntrySelected = this.onEntrySelected.bind(this, this.networkDataProvider); this.mainFlameChart.addEventListener(PerfUI.FlameChart.Events.EntrySelected, this.onMainEntrySelected, this); this.mainFlameChart.addEventListener(PerfUI.FlameChart.Events.EntryInvoked, this.onMainEntrySelected, this); this.networkFlameChart.addEventListener(PerfUI.FlameChart.Events.EntrySelected, this.onNetworkEntrySelected, this); this.networkFlameChart.addEventListener(PerfUI.FlameChart.Events.EntryInvoked, this.onNetworkEntrySelected, this); this.mainFlameChart.addEventListener(PerfUI.FlameChart.Events.EntryHighlighted, this.onEntryHighlighted, this); this.boundRefresh = this.refresh.bind(this); this.#selectedEvents = null; this.mainDataProvider.setEventColorMapping(TimelineUIUtils.eventColor); this.groupBySetting = Common.Settings.Settings.instance().createSetting( 'timelineTreeGroupBy', AggregatedTimelineTreeView.GroupBy.None); this.groupBySetting.addChangeListener(this.updateColorMapper, this); this.updateColorMapper(); } updateColorMapper(): void { if (!this.model) { return; } this.mainDataProvider.setEventColorMapping(TimelineUIUtils.eventColor); this.mainFlameChart.update(); } private onWindowChanged(event: Common.EventTarget.EventTargetEvent<WindowChangedEvent>): void { const {window, animate} = event.data; this.mainFlameChart.setWindowTimes(window.left, window.right, animate); this.networkFlameChart.setWindowTimes(window.left, window.right, animate); this.networkDataProvider.setWindowTimes(window.left, window.right); this.updateSearchResults(false, false); } windowChanged(windowStartTime: number, windowEndTime: number, animate: boolean): void { if (this.model) { this.model.setWindow({left: windowStartTime, right: windowEndTime}, animate); } } updateRangeSelection(startTime: number, endTime: number): void { this.delegate.select(TimelineSelection.fromRange(startTime, endTime)); } updateSelectedGroup(flameChart: PerfUI.FlameChart.FlameChart, group: PerfUI.FlameChart.Group|null): void { if (flameChart !== this.mainFlameChart) { return; } this.#selectedEvents = group ? this.mainDataProvider.groupTreeEvents(group) : null; this.updateTrack(); } setModel( model: PerformanceModel|null, newTraceEngineData: TraceEngine.TraceModel.PartialTraceParseDataDuringMigration|null): void { if (model === this.model) { return; } this.#traceEngineData = newTraceEngineData; Common.EventTarget.removeEventListeners(this.eventListeners); this.model = model; this.#selectedEvents = null; this.mainDataProvider.setModel(this.model, newTraceEngineData); this.networkDataProvider.setModel(this.model, newTraceEngineData); if (this.model) { this.eventListeners = [ this.model.addEventListener(PerformanceModelEvents.WindowChanged, this.onWindowChanged, this), ]; const window = this.model.window(); this.mainFlameChart.setWindowTimes(window.left, window.right); this.networkFlameChart.setWindowTimes(window.left, window.right); this.networkDataProvider.setWindowTimes(window.left, window.right); this.updateSearchResults(false, false); } this.updateColorMapper(); this.updateTrack(); this.refresh(); } private updateTrack(): void { this.countersView.setModel(this.model, this.#selectedEvents); this.detailsView.setModel(this.model, this.#traceEngineData, this.#selectedEvents); } private refresh(): void { if (this.networkDataProvider.isEmpty()) { this.mainFlameChart.enableRuler(true); this.networkSplitWidget.hideSidebar(); } else { this.mainFlameChart.enableRuler(false); this.networkSplitWidget.showBoth(); this.resizeToPreferredHeights(); } this.mainFlameChart.reset(); this.networkFlameChart.reset(); this.updateSearchResults(false, false); } private onEntryHighlighted(commonEvent: Common.EventTarget.EventTargetEvent<number>): void { SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight(); const entryIndex = commonEvent.data; // TODO(crbug.com/1431166): explore how we can make highlighting agnostic // and take either legacy events, or new trace engine events. Currently if // this highlight comes from a TrackAppender, we create a new legacy event // from the event payload, mainly to satisfy this method. const event = this.mainDataProvider.eventByIndex(entryIndex); if (!event) { return; } const target = this.model && this.model.timelineModel().targetByEvent(event); if (!target) { return; } let backendNodeIds; // Events for tracks that are migrated to the new engine won't use // TimelineModel.TimelineData. if (event instanceof SDK.TracingModel.Event) { const timelineData = TimelineModel.TimelineModel.EventOnTimelineData.forEvent(event); backendNodeIds = timelineData.backendNodeIds; } else if (TraceEngine.Types.TraceEvents.isTraceEventLayoutShift(event)) { const impactedNodes = event.args.data?.impacted_nodes ?? []; backendNodeIds = impactedNodes.map(node => node.node_id); } if (!backendNodeIds) { return; } for (let i = 0; i < backendNodeIds.length; ++i) { new SDK.DOMModel.DeferredDOMNode(target, backendNodeIds[i]).highlight(); } } highlightEvent(event: SDK.TracingModel.Event|null): void { const entryIndex = event ? this.mainDataProvider.entryIndexForSelection(TimelineSelection.fromTraceEvent(event)) : -1; if (entryIndex >= 0) { this.mainFlameChart.highlightEntry(entryIndex); } else { this.mainFlameChart.hideHighlight(); } } override willHide(): void { this.networkFlameChartGroupExpansionSetting.removeChangeListener(this.resizeToPreferredHeights, this); this.showMemoryGraphSetting.removeChangeListener(this.updateCountersGraphToggle, this); Bindings.IgnoreListManager.IgnoreListManager.instance().removeChangeListener(this.boundRefresh); } override wasShown(): void { this.networkFlameChartGroupExpansionSetting.addChangeListener(this.resizeToPreferredHeights, this); this.showMemoryGraphSetting.addChangeListener(this.updateCountersGraphToggle, this); Bindings.IgnoreListManager.IgnoreListManager.instance().addChangeListener(this.boundRefresh); if (this.needsResizeToPreferredHeights) { this.resizeToPreferredHeights(); } this.mainFlameChart.scheduleUpdate(); this.networkFlameChart.scheduleUpdate(); } private updateCountersGraphToggle(): void { if (this.showMemoryGraphSetting.get()) { this.chartSplitWidget.showBoth(); } else { this.chartSplitWidget.hideSidebar(); } } setSelection(selection: TimelineSelection|null): void { let index = this.mainDataProvider.entryIndexForSelection(selection); this.mainFlameChart.setSelectedEntry(index); index = this.networkDataProvider.entryIndexForSelection(selection); this.networkFlameChart.setSelectedEntry(index); if (this.detailsView) { this.detailsView.setSelection(selection); } } private onEntrySelected( dataProvider: PerfUI.FlameChart.FlameChartDataProvider, event: Common.EventTarget.EventTargetEvent<number>): void { const entryIndex = event.data; if (Root.Runtime.experiments.isEnabled('timelineEventInitiators') && dataProvider === this.mainDataProvider) { if (this.mainDataProvider.buildFlowForInitiator(entryIndex)) { this.mainFlameChart.scheduleUpdate(); } } this.delegate.select((dataProvider as TimelineFlameChartNetworkDataProvider | TimelineFlameChartDataProvider) .createSelection(entryIndex)); } resizeToPreferredHeights(): void { if (!this.isShowing()) { this.needsResizeToPreferredHeights = true; return; } this.needsResizeToPreferredHeights = false; this.networkPane.element.classList.toggle( 'timeline-network-resizer-disabled', !this.networkDataProvider.isExpanded()); this.networkSplitWidget.setSidebarSize( this.networkDataProvider.preferredHeight() + this.splitResizer.clientHeight + PerfUI.FlameChart.HeaderHeight + 2); } setSearchableView(searchableView: UI.SearchableView.SearchableView): void { this.searchableView = searchableView; } // UI.SearchableView.Searchable implementation jumpToNextSearchResult(): void { if (!this.searchResults || !this.searchResults.length) { return; } const index = typeof this.selectedSearchResult !== 'undefined' ? this.searchResults.indexOf(this.selectedSearchResult) : -1; this.selectSearchResult(Platform.NumberUtilities.mod(index + 1, this.searchResults.length)); } jumpToPreviousSearchResult(): void { if (!this.searchResults || !this.searchResults.length) { return; } const index = typeof this.selectedSearchResult !== 'undefined' ? this.searchResults.indexOf(this.selectedSearchResult) : 0; this.selectSearchResult(Platform.NumberUtilities.mod(index - 1, this.searchResults.length)); } supportsCaseSensitiveSearch(): boolean { return true; } supportsRegexSearch(): boolean { return true; } private selectSearchResult(index: number): void { this.searchableView.updateCurrentMatchIndex(index); if (this.searchResults) { this.selectedSearchResult = this.searchResults[index]; this.delegate.select(this.mainDataProvider.createSelection(this.selectedSearchResult)); } } private updateSearchResults(shouldJump: boolean, jumpBackwards?: boolean): void { const oldSelectedSearchResult = (this.selectedSearchResult as number); delete this.selectedSearchResult; this.searchResults = []; if (!this.searchRegex || !this.model) { return; } const regExpFilter = new TimelineRegExp(this.searchRegex); const window = this.model.window(); this.searchResults = this.mainDataProvider.search(window.left, window.right, regExpFilter); this.searchableView.updateSearchMatchesCount(this.searchResults.length); if (!shouldJump || !this.searchResults.length) { return; } let selectedIndex = this.searchResults.indexOf(oldSelectedSearchResult); if (selectedIndex === -1) { selectedIndex = jumpBackwards ? this.searchResults.length - 1 : 0; } this.selectSearchResult(selectedIndex); } /** * Returns the indexes of the elements that matched the most recent * query. Elements are indexed by the data provider and correspond * to their position in the data provider entry data array. * Public only for tests. */ getSearchResults(): number[]|undefined { return this.searchResults; } onSearchCanceled(): void { if (typeof this.selectedSearchResult !== 'undefined') { this.delegate.select(null); } delete this.searchResults; delete this.selectedSearchResult; delete this.searchRegex; } performSearch(searchConfig: UI.SearchableView.SearchConfig, shouldJump: boolean, jumpBackwards?: boolean): void { this.searchRegex = searchConfig.toSearchRegex().regex; this.updateSearchResults(shouldJump, jumpBackwards); } } export class Selection { timelineSelection: TimelineSelection; entryIndex: number; constructor(selection: TimelineSelection, entryIndex: number) { this.timelineSelection = selection; this.entryIndex = entryIndex; } } export const FlameChartStyle = { textColor: '#333', }; export class TimelineFlameChartMarker implements PerfUI.FlameChart.FlameChartMarker { private readonly startTimeInternal: number; private readonly startOffset: number; private style: TimelineMarkerStyle; constructor(startTime: number, startOffset: number, style: TimelineMarkerStyle) { this.startTimeInternal = startTime; this.startOffset = startOffset; this.style = style; } startTime(): number { return this.startTimeInternal; } color(): string { return this.style.color; } title(): string|null { if (this.style.lowPriority) { return null; } const startTime = i18n.TimeUtilities.millisToString(this.startOffset); return i18nString(UIStrings.sAtS, {PH1: this.style.title, PH2: startTime}); } draw(context: CanvasRenderingContext2D, x: number, height: number, pixelsPerMillisecond: number): void { const lowPriorityVisibilityThresholdInPixelsPerMs = 4; if (this.style.lowPriority && pixelsPerMillisecond < lowPriorityVisibilityThresholdInPixelsPerMs) { return; } context.save(); if (this.style.tall) { context.strokeStyle = this.style.color; context.lineWidth = this.style.lineWidth; context.translate(this.style.lineWidth < 1 || (this.style.lineWidth & 1) ? 0.5 : 0, 0.5); context.beginPath(); context.moveTo(x, 0); context.setLineDash(this.style.dashStyle); context.lineTo(x, context.canvas.height); context.stroke(); } context.restore(); } } // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export enum ColorBy { URL = 'URL', }