UNPKG

chrome-devtools-frontend

Version:
595 lines (525 loc) • 25.1 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 PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js'; import * as UI from '../../ui/legacy/legacy.js'; import {CountersGraph} from './CountersGraph.js'; import type {PerformanceModel, WindowChangedEvent} from './PerformanceModel.js'; import {Events as PerformanceModelEvents} 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 './TimelinePanel.js'; import {AggregatedTimelineTreeView} from './TimelineTreeView.js'; import type {TimelineMarkerStyle} from './TimelineUIUtils.js'; import {TimelineUIUtils} from './TimelineUIUtils.js'; import {WebVitalsIntegrator} from './WebVitalsTimelineUtils.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_); class MainSplitWidget extends UI.SplitWidget.SplitWidget { private webVitals!: WebVitalsIntegrator; private model!: PerformanceModel|null; constructor( isVertical: boolean, secondIsSidebar: boolean, settingName?: string, defaultSidebarWidth?: number, defaultSidebarHeight?: number, constraintsInDip?: boolean) { super(isVertical, secondIsSidebar, settingName, defaultSidebarWidth, defaultSidebarHeight, constraintsInDip); } setWebVitals(webVitals: WebVitalsIntegrator): void { this.webVitals = webVitals; this.webVitals.setMinimumSize(0, 120); } setWindowTimes(left: number, right: number, animate: boolean): void { if (!this.webVitals) { return; } const startTime = left - (this.model ? this.model.timelineModel().minimumRecordTime() : 0); this.webVitals.chartViewport.setWindowTimes(left, right, animate); this.webVitals.webVitalsTimeline.data = { startTime: startTime, duration: right - left, fcps: undefined, lcps: undefined, layoutShifts: undefined, longTasks: undefined, mainFrameNavigations: undefined, maxDuration: undefined, }; } setModelAndUpdateBoundaries(model: PerformanceModel|null): void { this.model = model; if (!this.webVitals || !model) { return; } const left = model.window().left; const right = model.window().right; const timelineModel = model.timelineModel(); const events: SDK.TracingModel.Event[] = timelineModel.tracks().reduce((prev, curr) => prev.concat(curr.events), ([] as SDK.TracingModel.Event[])); const minimumBoundary = model.timelineModel().minimumRecordTime(); const prepareEvents = (filterFunction: (arg0: SDK.TracingModel.Event) => boolean): number[] => events.filter(filterFunction).map(e => e.startTime - minimumBoundary); const lcpEvents = events.filter(e => timelineModel.isLCPCandidateEvent(e) || timelineModel.isLCPInvalidateEvent(e)); const lcpEventsByNavigationId = new Map<string, SDK.TracingModel.Event>(); for (const e of lcpEvents) { const navigationId = e.args['data']['navigationId']; const previousLastEvent = lcpEventsByNavigationId.get(navigationId); if (!previousLastEvent || previousLastEvent.args['data']['candidateIndex'] < e.args['data']['candidateIndex']) { lcpEventsByNavigationId.set(navigationId, e); } } const latestLcpCandidatesByNavigationId = Array.from(lcpEventsByNavigationId.values()); const latestLcpEvents = latestLcpCandidatesByNavigationId.filter(e => timelineModel.isLCPCandidateEvent(e)); const longTasks = events.filter(e => SDK.TracingModel.TracingModel.isCompletePhase(e.phase) && timelineModel.isLongRunningTask(e)) .map(e => ({start: e.startTime - minimumBoundary, duration: e.duration || 0})); this.webVitals.chartViewport.setBoundaries(left, right - left); this.webVitals.chartViewport.setWindowTimes(left, right); const startTime = left - (this.model ? this.model.timelineModel().minimumRecordTime() : 0); this.webVitals.webVitalsTimeline.data = { startTime: startTime, duration: right - left, maxDuration: timelineModel.maximumRecordTime(), fcps: events.filter(e => timelineModel.isFCPEvent(e)).map(e => ({timestamp: e.startTime - minimumBoundary, e})), lcps: latestLcpEvents.map(e => e.startTime).map(t => ({timestamp: t - minimumBoundary})), layoutShifts: prepareEvents(e => timelineModel.isLayoutShiftEvent(e)).map(t => ({timestamp: t})), longTasks, mainFrameNavigations: prepareEvents(e => timelineModel.isMainFrameNavigationStartEvent(e)), }; } } 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>; // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // eslint-disable-next-line @typescript-eslint/no-explicit-any private readonly showWebVitalsSetting: 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 webVitals: WebVitalsIntegrator; private readonly mainSplitWidget: MainSplitWidget; 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 nextExtensionIndex: number; private readonly boundRefresh: () => void; private selectedTrack: TimelineModel.TimelineModel.Track|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 urlToColorCache?: Map<Platform.DevToolsPath.UrlString, string>; private needsResizeToPreferredHeights?: boolean; private selectedSearchResult?: number; private searchRegex?: RegExp; constructor(delegate: TimelineModeViewDelegate) { super(); this.element.classList.add('timeline-flamechart'); this.delegate = delegate; this.model = null; this.eventListeners = []; this.showMemoryGraphSetting = Common.Settings.Settings.instance().createSetting('timelineShowMemory', false); this.showWebVitalsSetting = Common.Settings.Settings.instance().createSetting('timelineWebVitals', 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 web vitals / 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.networkFlameChart.disableRangeSelection(); 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.webVitals = new WebVitalsIntegrator(this); this.mainSplitWidget = new MainSplitWidget(false, false, 'timelineFlamechartMainAndVitalsView', undefined, 120); this.mainSplitWidget.setWebVitals(this.webVitals); this.mainSplitWidget.setMainWidget(this.mainFlameChart); this.mainSplitWidget.setSidebarWidget(this.webVitals); this.toggleWebVitalsLane(); this.networkSplitWidget.setMainWidget(this.mainSplitWidget); 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.nextExtensionIndex = 0; this.boundRefresh = this.refresh.bind(this); this.selectedTrack = 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(); } toggleWebVitalsLane(): void { if (this.showWebVitalsSetting.get()) { this.mainSplitWidget.showBoth(); this.mainSplitWidget.setSidebarSize(120); this.mainSplitWidget.setResizable(false); this.mainSplitWidget.hideDefaultResizer(true); } else { this.mainSplitWidget.hideSidebar(); } } private updateColorMapper(): void { this.urlToColorCache = new Map(); 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.mainSplitWidget.setWindowTimes(window.left, window.right, Boolean(animate)); 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; } const track = group ? this.mainDataProvider.groupTrack(group) : null; this.selectedTrack = track; this.updateTrack(); } setModel(model: PerformanceModel|null): void { if (model === this.model) { return; } Common.EventTarget.removeEventListeners(this.eventListeners); this.model = model; this.selectedTrack = null; this.mainDataProvider.setModel(this.model); this.networkDataProvider.setModel(this.model); if (this.model) { this.eventListeners = [ this.model.addEventListener(PerformanceModelEvents.WindowChanged, this.onWindowChanged, this), this.model.addEventListener(PerformanceModelEvents.ExtensionDataAdded, this.appendExtensionData, 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.mainSplitWidget.setModelAndUpdateBoundaries(model); this.updateSearchResults(false, false); } this.updateColorMapper(); this.updateTrack(); this.nextExtensionIndex = 0; this.appendExtensionData(); this.refresh(); } private updateTrack(): void { this.countersView.setModel(this.model, this.selectedTrack); this.detailsView.setModel(this.model, this.selectedTrack); } 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 appendExtensionData(): void { if (!this.model) { return; } const extensions = this.model.extensionInfo(); while (this.nextExtensionIndex < extensions.length) { this.mainDataProvider.appendExtensionEvents(extensions[this.nextExtensionIndex++]); } this.mainFlameChart.scheduleUpdate(); } private onEntryHighlighted(commonEvent: Common.EventTarget.EventTargetEvent<number>): void { SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight(); const entryIndex = commonEvent.data; const event = this.mainDataProvider.eventByIndex(entryIndex); if (!event) { return; } const target = this.model && this.model.timelineModel().targetByEvent(event); if (!target) { return; } const timelineData = TimelineModel.TimelineModel.TimelineData.forEvent(event); const backendNodeIds = timelineData.backendNodeIds; 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(); } } willHide(): void { this.networkFlameChartGroupExpansionSetting.removeChangeListener(this.resizeToPreferredHeights, this); this.showMemoryGraphSetting.removeChangeListener(this.updateCountersGraphToggle, this); Bindings.IgnoreListManager.IgnoreListManager.instance().removeChangeListener(this.boundRefresh); } 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).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); } searchCanceled(): 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', }