UNPKG

chrome-devtools-frontend

Version:
1,163 lines (1,044 loc) 123 kB
// Copyright 2021 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 */ /* * Copyright (C) 2012 Google Inc. All rights reserved. * Copyright (C) 2012 Intel 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. */ import '../../ui/legacy/legacy.js'; 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 type * as Protocol from '../../generated/protocol.js'; import * as CrUXManager from '../../models/crux-manager/crux-manager.js'; import * as Trace from '../../models/trace/trace.js'; import * as Workspace from '../../models/workspace/workspace.js'; import * as TraceBounds from '../../services/trace_bounds/trace_bounds.js'; import * as Adorners from '../../ui/components/adorners/adorners.js'; import * as Dialogs from '../../ui/components/dialogs/dialogs.js'; import * as LegacyWrapper from '../../ui/components/legacy_wrapper/legacy_wrapper.js'; import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as ThemeSupport from '../../ui/legacy/theme_support/theme_support.js'; import * as VisualLogging from '../../ui/visual_logging/visual_logging.js'; import * as MobileThrottling from '../mobile_throttling/mobile_throttling.js'; import {ActiveFilters} from './ActiveFilters.js'; import * as AnnotationHelpers from './AnnotationHelpers.js'; import {TraceLoadEvent} from './BenchmarkEvents.js'; import * as TimelineComponents from './components/components.js'; import * as TimelineInsights from './components/insights/insights.js'; import {Tracker} from './FreshRecording.js'; import {IsolateSelector} from './IsolateSelector.js'; import {AnnotationModifiedEvent, ModificationsManager} from './ModificationsManager.js'; import * as Overlays from './overlays/overlays.js'; import {cpuprofileJsonGenerator, traceJsonGenerator} from './SaveFileFormatter.js'; import {StatusDialog} from './StatusDialog.js'; import {type Client, TimelineController} from './TimelineController.js'; import {Tab} from './TimelineDetailsView.js'; import type {TimelineFlameChartDataProvider} from './TimelineFlameChartDataProvider.js'; import {Events as TimelineFlameChartViewEvents, TimelineFlameChartView} from './TimelineFlameChartView.js'; import {TimelineHistoryManager} from './TimelineHistoryManager.js'; import {TimelineLoader} from './TimelineLoader.js'; import {TimelineMiniMap} from './TimelineMiniMap.js'; import timelinePanelStyles from './timelinePanel.css.js'; import { rangeForSelection, selectionFromEvent, selectionIsRange, selectionsEqual, type TimelineSelection, } from './TimelineSelection.js'; import {TimelineUIUtils} from './TimelineUIUtils.js'; import {UIDevtoolsController} from './UIDevtoolsController.js'; import {UIDevtoolsUtils} from './UIDevtoolsUtils.js'; import * as Utils from './utils/utils.js'; const UIStrings = { /** *@description Text that appears when user drag and drop something (for example, a file) in Timeline Panel of the Performance panel */ dropTimelineFileOrUrlHere: 'Drop timeline file or URL here', /** *@description Title of capture layers and pictures setting in timeline panel of the performance panel */ enableAdvancedPaint: 'Enable advanced paint instrumentation (slow)', /** *@description Title of CSS selector stats setting in timeline panel of the performance panel */ enableSelectorStats: 'Enable CSS selector stats (slow)', /** *@description Title of show screenshots setting in timeline panel of the performance panel */ screenshots: 'Screenshots', /** *@description Text for the memory of the page */ memory: 'Memory', /** *@description Text to clear content */ clear: 'Clear', /** *@description A label for a button that fixes something. */ fixMe: 'Fix me', /** *@description Tooltip text that appears when hovering over the largeicon load button */ loadProfile: 'Load profile…', /** *@description Tooltip text that appears when hovering over the largeicon download button */ saveProfile: 'Save profile…', /** *@description An option to save trace with annotations that appears in the menu of the toolbar download button. This is the expected default option, therefore it does not mention annotations. */ saveTraceWithAnnotationsMenuOption: 'Save trace', /** *@description An option to save trace without annotations that appears in the menu of the toolbar download button */ saveTraceWithoutAnnotationsMenuOption: 'Save trace without annotations', /** *@description Text to take screenshots */ captureScreenshots: 'Capture screenshots', /** *@description Text in Timeline Panel of the Performance panel */ showMemoryTimeline: 'Show memory timeline', /** *@description Tooltip text that appears when hovering over the largeicon settings gear in show settings pane setting in timeline panel of the performance panel */ captureSettings: 'Capture settings', /** *@description Text in Timeline Panel of the Performance panel */ capturesAdvancedPaint: 'Captures advanced paint instrumentation, introduces significant performance overhead', /** *@description Text in Timeline Panel of the Performance panel */ capturesSelectorStats: 'Captures CSS selector statistics', /** *@description Text in Timeline Panel of the Performance panel */ network: 'Network:', /** *@description Text in Timeline Panel of the Performance panel */ cpu: 'CPU:', /** *@description Title of the 'Network conditions' tool in the bottom drawer */ networkConditions: 'Network conditions', /** *@description Text in Timeline Panel of the Performance panel */ CpuThrottlingIsEnabled: '- CPU throttling is enabled', /** *@description Text in Timeline Panel of the Performance panel */ NetworkThrottlingIsEnabled: '- Network throttling is enabled', /** *@description Text in Timeline Panel of the Performance panel */ SignificantOverheadDueToPaint: '- Significant overhead due to paint instrumentation', /** *@description Text in Timeline Panel of the Performance panel */ SelectorStatsEnabled: '- Selector stats is enabled', /** *@description Text in Timeline Panel of the Performance panel */ stoppingTimeline: 'Stopping timeline…', /** *@description Text in Timeline Panel of the Performance panel */ received: 'Received', /** *@description Text in Timeline Panel of the Performance panel */ processed: 'Processed', /** *@description Text to close something */ close: 'Close', /** *@description Status text to indicate the recording has failed in the Performance panel */ recordingFailed: 'Recording failed', /** *@description Status text to indicate that exporting the trace has failed */ exportingFailed: 'Exporting the trace failed', /** * @description Text to indicate the progress of a profile. Informs the user that we are currently * creating a peformance profile. */ profiling: 'Profiling…', /** *@description Text in Timeline Panel of the Performance panel */ bufferUsage: 'Buffer usage', /** *@description Text in Timeline Panel of the Performance panel */ loadingProfile: 'Loading profile…', /** *@description Text in Timeline Panel of the Performance panel */ processingProfile: 'Processing profile…', /** *@description Text in Timeline Panel of the Performance panel */ initializingProfiler: 'Initializing profiler…', /** * * @description Text for exporting basic traces */ exportNormalTraces: 'Basic performance traces', /** * * @description Text for exporting enhanced traces */ exportEnhancedTraces: 'Enhanced performance traces', /** *@description Tooltip description for a checkbox that toggles the visibility of data added by extensions of this panel (Performance). */ showDataAddedByExtensions: 'Show data added by extensions of the Performance panel', /** Label for a checkbox that toggles the visibility of data added by extensions of this panel (Performance). */ showCustomtracks: 'Show custom tracks', /** * @description Tooltip for the the sidebar toggle in the Performance panel. Command to open/show the sidebar. */ showSidebar: 'Show sidebar', /** * @description Tooltip for the the sidebar toggle in the Performance panel. Command to close the sidebar. */ hideSidebar: 'Hide sidebar', /** * @description Screen reader announcement when the sidebar is shown in the Performance panel. */ sidebarShown: 'Performance sidebar shown', /** * @description Screen reader announcement when the sidebar is hidden in the Performance panel. */ sidebarHidden: 'Performance sidebar hidden', /** * @description Screen reader announcement when the user clears their selection */ selectionCleared: 'Selection cleared', /** * @description Screen reader announcement when the user selects a frame. */ frameSelected: 'Frame selected', /** * @description Screen reader announcement when the user selects a trace event. * @example {Paint} PH1 */ eventSelected: 'Event {PH1} selected', /** *@description Text of a hyperlink to documentation. */ learnMore: 'Learn more', /** * @description Tooltip text for a button that takes the user back to the default view which shows performance metrics that are live. */ backToLiveMetrics: 'Go back to the live metrics page', /** * @description Description of the Timeline zoom keyboard instructions that appear in the shortcuts dialog */ timelineZoom: 'Zoom', /** * @description Description of the Timeline scrolling & panning instructions that appear in the shortcuts dialog. */ timelineScrollPan: 'Scroll & Pan', /** * @description Title for the Dim 3rd Parties checkbox. */ dimThirdParties: 'Dim 3rd parties', /** * @description Description for the Dim 3rd Parties checkbox tooltip describing how 3rd parties are classified. */ thirdPartiesByThirdPartyWeb: '3rd parties classified by third-party-web', /** * @description Title of the shortcuts dialog shown to the user that lists keyboard shortcuts. */ shortcutsDialogTitle: 'Keyboard shortcuts for flamechart' } as const; const str_ = i18n.i18n.registerUIStrings('panels/timeline/TimelinePanel.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); let timelinePanelInstance: TimelinePanel|undefined; let isNode: boolean; /** * Represents the states that the timeline panel can be in. * If you need to change the panel's view, use the {@see #changeView} method. * Note that we do not represent the "Loading/Processing" view here. The * StatusPane is managed in the code that handles file import/recording, and * when it is visible it is rendered on top of the UI so obscures what is behind * it. When it completes, we will set the view mode to the trace that has been * loaded. */ type ViewMode = { mode: 'LANDING_PAGE', }|{ mode: 'VIEWING_TRACE', traceIndex: number, }|{ mode: 'STATUS_PANE_OVERLAY', }; export class TimelinePanel extends Common.ObjectWrapper.eventMixin<EventTypes, typeof UI.Panel.Panel>(UI.Panel.Panel) implements Client, TimelineModeViewDelegate { private readonly dropTarget: UI.DropTarget.DropTarget; private readonly recordingOptionUIControls: UI.Toolbar.ToolbarItem[]; private state: State; private recordingPageReload: boolean; private readonly millisecondsToRecordAfterLoadEvent: number; private readonly toggleRecordAction: UI.ActionRegistration.Action; private readonly recordReloadAction: UI.ActionRegistration.Action; readonly #historyManager: TimelineHistoryManager; private readonly captureLayersAndPicturesSetting: Common.Settings.Setting<boolean>; private readonly captureSelectorStatsSetting: Common.Settings.Setting<boolean>; readonly #thirdPartyTracksSetting: Common.Settings.Setting<boolean>; private showScreenshotsSetting: Common.Settings.Setting<boolean>; private showMemorySetting: Common.Settings.Setting<boolean>; private readonly panelToolbar: UI.Toolbar.Toolbar; private readonly panelRightToolbar: UI.Toolbar.Toolbar; private readonly timelinePane: UI.Widget.VBox; readonly #minimapComponent = new TimelineMiniMap(); #viewMode: ViewMode = {mode: 'LANDING_PAGE'}; readonly #dimThirdPartiesSetting: Common.Settings.Setting<boolean>|null = null; #thirdPartyCheckbox: UI.Toolbar.ToolbarSettingCheckbox|null = null; /** * We get given any filters for a new trace when it is recorded/imported. * Because the user can then use the dropdown to navigate to another trace, * we store the filters by the trace index, so if the user then navigates back * to a previous trace we can reinstate the filters from this map. */ #exclusiveFilterPerTrace = new Map<number, Trace.Extras.TraceFilter.TraceFilter>(); /** * This widget holds the timeline sidebar which shows Insights & Annotations, * and the main UI which shows the timeline */ readonly #splitWidget = new UI.SplitWidget.SplitWidget( true, // isVertical false, // secondIsSidebar 'timeline-panel-sidebar-state', // settingName (to persist the open/closed state for the user) TimelineComponents.Sidebar.DEFAULT_SIDEBAR_WIDTH_PX, ); private readonly statusPaneContainer: HTMLElement; private readonly flameChart: TimelineFlameChartView; private readonly searchableViewInternal: UI.SearchableView.SearchableView; private showSettingsPaneButton!: UI.Toolbar.ToolbarSettingToggle; private showSettingsPaneSetting!: Common.Settings.Setting<boolean>; private settingsPane?: HTMLElement; private controller!: TimelineController|null; private cpuProfiler!: SDK.CPUProfilerModel.CPUProfilerModel|null; private clearButton!: UI.Toolbar.ToolbarButton; private loadButton!: UI.Toolbar.ToolbarButton; private saveButton!: UI.Toolbar.ToolbarButton|UI.Toolbar.ToolbarMenuButton; private homeButton?: UI.Toolbar.ToolbarButton; private statusDialog: StatusDialog|null = null; private landingPage!: UI.Widget.Widget; private loader?: TimelineLoader; private showScreenshotsToolbarCheckbox?: UI.Toolbar.ToolbarItem; private showMemoryToolbarCheckbox?: UI.Toolbar.ToolbarItem; private networkThrottlingSelect?: MobileThrottling.ThrottlingManager.NetworkThrottlingSelectorWrapper; private cpuThrottlingSelect?: MobileThrottling.ThrottlingManager.CPUThrottlingSelectorWrapper; private fileSelectorElement?: HTMLInputElement; private selection: TimelineSelection|null = null; private traceLoadStart!: Trace.Types.Timing.Milli|null; #traceEngineModel: Trace.TraceModel.Model; #sourceMapsResolver: Utils.SourceMapsResolver.SourceMapsResolver|null = null; #entityMapper: Utils.EntityMapper.EntityMapper|null = null; #onSourceMapsNodeNamesResolvedBound = this.#onSourceMapsNodeNamesResolved.bind(this); #sidebarToggleButton = this.#splitWidget.createShowHideSidebarButton( i18nString(UIStrings.showSidebar), i18nString(UIStrings.hideSidebar), // These are used to announce to screen-readers and not shown visibly. i18nString(UIStrings.sidebarShown), i18nString(UIStrings.sidebarHidden), 'timeline.sidebar', // jslog context ); #sideBar = new TimelineComponents.Sidebar.SidebarWidget(); /** * Rather than auto-pop the sidebar every time the user records a trace, * which could get annoying, we instead persist the state of the sidebar * visibility to a setting so it's restored across sessions. * However, sometimes we have to automatically hide the sidebar, like when a * trace recording is happening, or the user is on the landing page. In those * times, we toggle this flag to true. Then, when we enter the VIEWING_TRACE * mode, we check this flag and pop the sidebar open if it's set to true. * Longer term a better fix here would be to divide the 3 UI screens * (status pane, landing page, trace view) into distinct components / * widgets, to avoid this complexity. */ #restoreSidebarVisibilityOnTraceLoad = false; /** * Used to track an aria announcement that we need to alert for * screen-readers. We track these because we debounce announcements to not * overwhelm. */ #pendingAriaMessage: string|null = null; #eventToRelatedInsights: TimelineComponents.RelatedInsightChips.EventToRelatedInsightsMap = new Map(); #shortcutsDialog: Dialogs.ShortcutDialog.ShortcutDialog = new Dialogs.ShortcutDialog.ShortcutDialog(); /** * Track if the user has opened the shortcuts dialog before. We do this so that the * very first time the performance panel is open after the shortcuts dialog ships, we can * automatically pop it open to aid discovery. */ #userHadShortcutsDialogOpenedOnce = Common.Settings.Settings.instance().createSetting<boolean>( 'timeline.user-had-shortcuts-dialog-opened-once', false); /** * Navigation radio buttons located in the shortcuts dialog. */ #navigationRadioButtons = document.createElement('form'); #modernNavRadioButton = UI.UIUtils.createRadioButton( 'flamechart-selected-navigation', 'Modern - normal scrolling', 'timeline.select-modern-navigation'); #classicNavRadioButton = UI.UIUtils.createRadioButton( 'flamechart-selected-navigation', 'Classic - scroll to zoom', 'timeline.select-classic-navigation'); #onMainEntryHovered: (event: Common.EventTarget.EventTargetEvent<number>) => void; constructor(traceModel?: Trace.TraceModel.Model) { super('timeline'); this.registerRequiredCSS(timelinePanelStyles); const adornerContent = document.createElement('span'); adornerContent.innerHTML = `<div style=" font-size: 12px; transform: scale(1.25); color: transparent; background: linear-gradient(90deg,CLICK255 0 0 / 100%) 0%, rgb(255 154 0 / 100%) 10%, rgb(208 222 33 / 100%) 20%, rgb(79 220 74 / 100%) 30%, rgb(63 218 216 / 100%) 40%, rgb(47 201 226 / 100%) 50%, rgb(28 127 238 / 100%) 60%, rgb(95 21 242 / 100%) 70%, rgb(186 12 248 / 100%) 80%, rgb(251 7 217 / 100%) 90%, rgb(255 0 0 / 100%) 100%); -webkit-background-clip: text; ">💫</div>`; const adorner = new Adorners.Adorner.Adorner(); adorner.classList.add('fix-perf-icon'); adorner.data = { name: i18nString(UIStrings.fixMe), content: adornerContent, }; this.#traceEngineModel = traceModel || this.#instantiateNewModel(); this.#listenForProcessingProgress(); this.element.addEventListener('contextmenu', this.contextMenu.bind(this), false); this.dropTarget = new UI.DropTarget.DropTarget( this.element, [UI.DropTarget.Type.File, UI.DropTarget.Type.URI], i18nString(UIStrings.dropTimelineFileOrUrlHere), this.handleDrop.bind(this)); this.recordingOptionUIControls = []; this.state = State.IDLE; this.recordingPageReload = false; this.millisecondsToRecordAfterLoadEvent = 5000; this.toggleRecordAction = UI.ActionRegistry.ActionRegistry.instance().getAction('timeline.toggle-recording'); this.recordReloadAction = UI.ActionRegistry.ActionRegistry.instance().getAction('timeline.record-reload'); this.#historyManager = new TimelineHistoryManager(this.#minimapComponent, isNode); this.traceLoadStart = null; this.captureLayersAndPicturesSetting = Common.Settings.Settings.instance().createSetting( 'timeline-capture-layers-and-pictures', false, Common.Settings.SettingStorageType.SESSION); this.captureLayersAndPicturesSetting.setTitle(i18nString(UIStrings.enableAdvancedPaint)); this.captureSelectorStatsSetting = Common.Settings.Settings.instance().createSetting( 'timeline-capture-selector-stats', false, Common.Settings.SettingStorageType.SESSION); this.captureSelectorStatsSetting.setTitle(i18nString(UIStrings.enableSelectorStats)); this.showScreenshotsSetting = Common.Settings.Settings.instance().createSetting('timeline-show-screenshots', isNode ? false : true); this.showScreenshotsSetting.setTitle(i18nString(UIStrings.screenshots)); this.showScreenshotsSetting.addChangeListener(this.updateMiniMap, this); this.showMemorySetting = Common.Settings.Settings.instance().createSetting( 'timeline-show-memory', false, Common.Settings.SettingStorageType.SESSION); this.showMemorySetting.setTitle(i18nString(UIStrings.memory)); this.showMemorySetting.addChangeListener(this.onMemoryModeChanged, this); this.#dimThirdPartiesSetting = Common.Settings.Settings.instance().createSetting( 'timeline-dim-third-parties', false, Common.Settings.SettingStorageType.SESSION); this.#dimThirdPartiesSetting.setTitle(i18nString(UIStrings.dimThirdParties)); this.#dimThirdPartiesSetting.addChangeListener(this.onDimThirdPartiesChanged, this); this.#thirdPartyTracksSetting = TimelinePanel.extensionDataVisibilitySetting(); this.#thirdPartyTracksSetting.addChangeListener(this.#extensionDataVisibilityChanged, this); this.#thirdPartyTracksSetting.setTitle(i18nString(UIStrings.showCustomtracks)); const timelineToolbarContainer = this.element.createChild('div', 'timeline-toolbar-container'); timelineToolbarContainer.setAttribute('jslog', `${VisualLogging.toolbar()}`); timelineToolbarContainer.role = 'toolbar'; this.panelToolbar = timelineToolbarContainer.createChild('devtools-toolbar', 'timeline-main-toolbar'); this.panelToolbar.role = 'presentation'; this.panelToolbar.wrappable = true; this.panelRightToolbar = timelineToolbarContainer.createChild('devtools-toolbar'); this.panelRightToolbar.role = 'presentation'; if (!isNode) { this.createSettingsPane(); this.updateShowSettingsToolbarButton(); } this.timelinePane = new UI.Widget.VBox(); const topPaneElement = this.timelinePane.element.createChild('div', 'hbox'); topPaneElement.id = 'timeline-overview-panel'; this.#minimapComponent.show(topPaneElement); this.#minimapComponent.addEventListener(PerfUI.TimelineOverviewPane.Events.OVERVIEW_PANE_MOUSE_MOVE, event => { this.flameChart.addTimestampMarkerOverlay(event.data.timeInMicroSeconds); }); this.#minimapComponent.addEventListener(PerfUI.TimelineOverviewPane.Events.OVERVIEW_PANE_MOUSE_LEAVE, async () => { await this.flameChart.removeTimestampMarkerOverlay(); }); this.statusPaneContainer = this.timelinePane.element.createChild('div', 'status-pane-container fill'); this.createFileSelector(); SDK.TargetManager.TargetManager.instance().addModelListener( SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.Load, this.loadEventFired, this); this.flameChart = new TimelineFlameChartView(this); this.element.addEventListener( 'toggle-popover', event => this.flameChart.togglePopover((event as CustomEvent).detail)); this.#onMainEntryHovered = this.#onEntryHovered.bind(this, this.flameChart.getMainDataProvider()); this.flameChart.getMainFlameChart().addEventListener( PerfUI.FlameChart.Events.ENTRY_HOVERED, this.#onMainEntryHovered); this.flameChart.addEventListener(TimelineFlameChartViewEvents.ENTRY_LABEL_ANNOTATION_CLICKED, event => { const selection = selectionFromEvent(event.data.entry); this.select(selection); }); this.searchableViewInternal = new UI.SearchableView.SearchableView(this.flameChart, null); this.searchableViewInternal.setMinimumSize(0, 100); this.searchableViewInternal.setMinimalSearchQuerySize(2); // At 1 it can introduce a bit of jank. this.searchableViewInternal.element.classList.add('searchable-view'); this.searchableViewInternal.show(this.timelinePane.element); this.flameChart.show(this.searchableViewInternal.element); this.flameChart.setSearchableView(this.searchableViewInternal); this.searchableViewInternal.hideWidget(); this.#splitWidget.setMainWidget(this.timelinePane); this.#splitWidget.setSidebarWidget(this.#sideBar); this.#splitWidget.enableShowModeSaving(); this.#splitWidget.show(this.element); this.flameChart.overlays().addEventListener(Overlays.Overlays.TimeRangeMouseOverEvent.eventName, event => { const {overlay} = event as Overlays.Overlays.TimeRangeMouseOverEvent; const overlayBounds = Overlays.Overlays.traceWindowContainingOverlays([overlay]); if (!overlayBounds) { return; } this.#minimapComponent.highlightBounds(overlayBounds, /* withBracket */ false); }); this.flameChart.overlays().addEventListener(Overlays.Overlays.TimeRangeMouseOutEvent.eventName, () => { this.#minimapComponent.clearBoundsHighlight(); }); this.#sideBar.element.addEventListener(TimelineInsights.SidebarInsight.InsightDeactivated.eventName, () => { this.#setActiveInsight(null); }); this.#sideBar.element.addEventListener(TimelineInsights.SidebarInsight.InsightActivated.eventName, event => { const {model, insightSetKey} = event; this.#setActiveInsight({model, insightSetKey}); // Open the summary panel for the 3p insight. if (model.insightKey === Trace.Insights.Types.InsightKeys.THIRD_PARTIES) { void window.scheduler.postTask(() => { this.#openSummaryTab(); }, {priority: 'background'}); } }); this.#sideBar.element.addEventListener(TimelineInsights.SidebarInsight.InsightProvideOverlays.eventName, event => { const {overlays, options} = event; void window.scheduler.postTask(() => { this.flameChart.setOverlays(overlays, options); const overlaysBounds = Overlays.Overlays.traceWindowContainingOverlays(overlays); if (overlaysBounds) { this.#minimapComponent.highlightBounds(overlaysBounds, /* withBracket */ true); } else { this.#minimapComponent.clearBoundsHighlight(); } }, {priority: 'user-visible'}); }); this.#sideBar.contentElement.addEventListener(TimelineInsights.EventRef.EventReferenceClick.eventName, event => { this.select(selectionFromEvent(event.event)); }); this.#sideBar.element.addEventListener(TimelineComponents.Sidebar.RemoveAnnotation.eventName, event => { const {removedAnnotation} = (event as TimelineComponents.Sidebar.RemoveAnnotation); ModificationsManager.activeManager()?.removeAnnotation(removedAnnotation); }); this.#sideBar.element.addEventListener(TimelineComponents.Sidebar.RevealAnnotation.eventName, event => { this.flameChart.revealAnnotation(event.annotation); }); this.#sideBar.element.addEventListener(TimelineInsights.SidebarInsight.InsightSetHovered.eventName, event => { if (event.bounds) { this.#minimapComponent.highlightBounds(event.bounds, /* withBracket */ true); } else { this.#minimapComponent.clearBoundsHighlight(); } }); this.#sideBar.element.addEventListener(TimelineInsights.SidebarInsight.InsightSetZoom.eventName, event => { TraceBounds.TraceBounds.BoundsManager.instance().setTimelineVisibleWindow( event.bounds, {ignoreMiniMapBounds: true, shouldAnimate: true}); }); this.onMemoryModeChanged(); this.populateToolbar(); // The viewMode is set by default to the landing page, so we don't call // `#changeView` here and can instead directly call showLandingPage(); this.#showLandingPage(); this.updateTimelineControls(); SDK.TargetManager.TargetManager.instance().addEventListener( SDK.TargetManager.Events.SUSPEND_STATE_CHANGED, this.onSuspendStateChanged, this); const profilerModels = SDK.TargetManager.TargetManager.instance().models(SDK.CPUProfilerModel.CPUProfilerModel); for (const model of profilerModels) { for (const message of model.registeredConsoleProfileMessages) { this.consoleProfileFinished(message); } } SDK.TargetManager.TargetManager.instance().observeModels( SDK.CPUProfilerModel.CPUProfilerModel, { modelAdded: (model: SDK.CPUProfilerModel.CPUProfilerModel) => { model.addEventListener( SDK.CPUProfilerModel.Events.CONSOLE_PROFILE_FINISHED, event => this.consoleProfileFinished(event.data)); }, modelRemoved: (_model: SDK.CPUProfilerModel.CPUProfilerModel) => { }, }, ); } /** * Activates an insight and ensures the sidebar is open too. * Pass `highlightInsight: true` to flash the insight with the background highlight colour. */ #setActiveInsight(insight: TimelineComponents.Sidebar.ActiveInsight|null, opts: { highlightInsight: boolean, } = {highlightInsight: false}): void { if (insight) { this.#splitWidget.showBoth(); } this.#sideBar.setActiveInsight(insight, {highlight: opts.highlightInsight}); this.flameChart.setActiveInsight(insight); if (insight) { const selectedInsight = new SelectedInsight(insight); UI.Context.Context.instance().setFlavor(SelectedInsight, selectedInsight); } else { UI.Context.Context.instance().setFlavor(SelectedInsight, null); } } /** * This disables the 3P checkbox in the toolbar. * If the checkbox was checked, we flip it to indeterminiate to communicate it doesn't currently apply. */ set3PCheckboxDisabled(disabled: boolean): void { this.#thirdPartyCheckbox?.applyEnabledState(!disabled); if (this.#dimThirdPartiesSetting?.get()) { this.#thirdPartyCheckbox?.setIndeterminate(disabled); } } static instance(opts: { forceNew: boolean|null, isNode: boolean, traceModel?: Trace.TraceModel.Model, }|undefined = {forceNew: null, isNode: false}): TimelinePanel { const {forceNew, isNode: isNodeMode} = opts; isNode = isNodeMode; if (!timelinePanelInstance || forceNew) { timelinePanelInstance = new TimelinePanel(opts.traceModel); } return timelinePanelInstance; } static removeInstance(): void { // TODO(crbug.com/358583420): Simplify attached data management // so that we don't have to maintain all of these singletons. Utils.SourceMapsResolver.SourceMapsResolver.clearResolvedNodeNames(); Trace.Helpers.SyntheticEvents.SyntheticEventsManager.reset(); TraceBounds.TraceBounds.BoundsManager.removeInstance(); ModificationsManager.reset(); ActiveFilters.removeInstance(); timelinePanelInstance = undefined; } #instantiateNewModel(): Trace.TraceModel.Model { const config = Trace.Types.Configuration.defaults(); config.showAllEvents = Root.Runtime.experiments.isEnabled('timeline-show-all-events'); config.includeRuntimeCallStats = Root.Runtime.experiments.isEnabled('timeline-v8-runtime-call-stats'); config.debugMode = Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.TIMELINE_DEBUG_MODE); return Trace.TraceModel.Model.createWithAllHandlers(config); } static extensionDataVisibilitySetting(): Common.Settings.Setting<boolean> { // Calling this multiple times doesn't recreate the setting. // Instead, after the second call, the cached setting is returned. return Common.Settings.Settings.instance().createSetting('timeline-show-extension-data', true); } override searchableView(): UI.SearchableView.SearchableView|null { return this.searchableViewInternal; } override wasShown(): void { super.wasShown(); UI.Context.Context.instance().setFlavor(TimelinePanel, this); // Record the performance tool load time. Host.userMetrics.panelLoaded('timeline', 'DevTools.Launch.Timeline'); const cruxManager = CrUXManager.CrUXManager.instance(); cruxManager.addEventListener(CrUXManager.Events.FIELD_DATA_CHANGED, this.#onFieldDataChanged, this); this.#onFieldDataChanged(); } override willHide(): void { UI.Context.Context.instance().setFlavor(TimelinePanel, null); this.#historyManager.cancelIfShowing(); const cruxManager = CrUXManager.CrUXManager.instance(); cruxManager.removeEventListener(CrUXManager.Events.FIELD_DATA_CHANGED, this.#onFieldDataChanged, this); } #onFieldDataChanged(): void { const recs = Utils.Helpers.getThrottlingRecommendations(); this.cpuThrottlingSelect?.updateRecommendedOption(recs.cpuOption); this.networkThrottlingSelect?.updateRecommendedConditions(recs.networkConditions); } loadFromEvents(events: Trace.Types.Events.Event[]): void { if (this.state !== State.IDLE) { return; } this.prepareToLoadTimeline(); this.loader = TimelineLoader.loadFromEvents(events, this); } loadFromTraceFile(traceFile: Trace.Types.File.TraceFile): void { if (this.state !== State.IDLE) { return; } this.prepareToLoadTimeline(); this.loader = TimelineLoader.loadFromTraceFile(traceFile, this); } getFlameChart(): TimelineFlameChartView { return this.flameChart; } getMinimap(): TimelineMiniMap { return this.#minimapComponent; } /** * Determine if two view modes are equivalent. Useful because if {@see * #changeView} gets called and the new mode is identical to the current, * we can bail without doing any UI updates. */ #viewModesEquivalent(m1: ViewMode, m2: ViewMode): boolean { if (m1.mode === 'LANDING_PAGE' && m2.mode === 'LANDING_PAGE') { return true; } if (m1.mode === 'STATUS_PANE_OVERLAY' && m2.mode === 'STATUS_PANE_OVERLAY') { return true; } // VIEWING_TRACE views are only equivalent if their traceIndex is the same. if (m1.mode === 'VIEWING_TRACE' && m2.mode === 'VIEWING_TRACE' && m1.traceIndex === m2.traceIndex) { return true; } return false; } #uninstallSourceMapsResolver(): void { if (this.#sourceMapsResolver) { // this set of NodeNames is cached by PIDs, so we clear it so we don't // use incorrect names from another trace that might happen to share // PID/TIDs. Utils.SourceMapsResolver.SourceMapsResolver.clearResolvedNodeNames(); this.#sourceMapsResolver.removeEventListener( Utils.SourceMapsResolver.SourceMappingsUpdated.eventName, this.#onSourceMapsNodeNamesResolvedBound); this.#sourceMapsResolver.uninstall(); this.#sourceMapsResolver = null; } } #removeStatusPane(): void { if (this.statusDialog) { this.statusDialog.remove(); } this.statusDialog = null; } hasActiveTrace(): boolean { return this.#viewMode.mode === 'VIEWING_TRACE'; } #changeView(newMode: ViewMode): void { if (this.#viewModesEquivalent(this.#viewMode, newMode)) { return; } if (this.#viewMode.mode === 'VIEWING_TRACE') { // If the current / about to be "old" view was viewing a trace // we also uninstall any source maps resolver for the trace that was active. // If the user swaps back to this trace via the history dropdown, this will be reinstated. this.#uninstallSourceMapsResolver(); // Store any modifications (e.g. annotations) that the user has created // on the current trace before we move away to a new view. this.#saveModificationsForActiveTrace(); } this.#viewMode = newMode; this.updateTimelineControls(); /** * Note that the TimelinePanel UI is really rendered in two distinct layers. * 1. status-pane-container: this is what renders both the StatusPane * loading modal AND the landing page. * What is important to note is that this renders ON TOP of the * SearchableView widget, which is what holds the FlameChartView. * * 2. SearchableView: this is the container that renders * TimelineFlameChartView and the rest of the flame chart code. * * What this layering means is that when we swap to the LANDING_PAGE or * STATUS_PANE_OVERLAY view, we don't actually need to reset the * SearchableView that is rendered behind it, because it won't be visible * and will be hidden behind the StatusPane/Landing Page. * * So the only time we update this SearchableView is when the user goes to * view a trace. That is why in the switch() statement below you won't see * any code that resets the SearchableView because we don't need to. We do * mark it as hidden, but mainly so the user can't accidentally use Cmd-F * to search a hidden view. */ switch (newMode.mode) { case 'LANDING_PAGE': { this.#removeStatusPane(); this.#showLandingPage(); this.dispatchEventToListeners(Events.IS_VIEWING_TRACE, false); // Whilst we don't reset this, we hide it, mainly so the user cannot // hit Ctrl/Cmd-F and try to search when it isn't visible. this.searchableViewInternal.hideWidget(); return; } case 'VIEWING_TRACE': { this.#hideLandingPage(); this.#setModelForActiveTrace(); this.#removeStatusPane(); this.#showSidebarIfRequired(); this.flameChart.dimThirdPartiesIfRequired(); this.dispatchEventToListeners(Events.IS_VIEWING_TRACE, true); return; } case 'STATUS_PANE_OVERLAY': { // We don't manage the StatusPane UI here; it is done in the // recordingStarted/recordingProgress callbacks, but we do make sure we // hide the landing page. this.#hideLandingPage(); this.dispatchEventToListeners(Events.IS_VIEWING_TRACE, false); // We also hide the sidebar - else if the user is viewing a trace and // then load/record another, the sidebar remains visible. this.#hideSidebar(); return; } default: Platform.assertNever(newMode, 'Unsupported TimelinePanel viewMode'); } } #activeTraceIndex(): number|null { if (this.#viewMode.mode === 'VIEWING_TRACE') { return this.#viewMode.traceIndex; } return null; } /** * NOTE: this method only exists to enable some layout tests to be migrated to the new engine. * DO NOT use this method within DevTools. It is marked as deprecated so * within DevTools you are warned when using the method. * @deprecated **/ getParsedTraceForLayoutTests(): Trace.Handlers.Types.ParsedTrace { const traceIndex = this.#activeTraceIndex(); if (traceIndex === null) { throw new Error('No trace index active.'); } const data = this.#traceEngineModel.parsedTrace(traceIndex); if (data === null) { throw new Error('No trace engine data found.'); } return data; } /** * NOTE: this method only exists to enable some layout tests to be migrated to the new engine. * DO NOT use this method within DevTools. It is marked as deprecated so * within DevTools you are warned when using the method. * @deprecated **/ getTraceEngineRawTraceEventsForLayoutTests(): readonly Trace.Types.Events.Event[] { const traceIndex = this.#activeTraceIndex(); if (traceIndex === null) { throw new Error('No trace index active.'); } const data = this.#traceEngineModel.rawTraceEvents(traceIndex); if (data === null) { throw new Error('No trace engine data found.'); } return data; } #onEntryHovered(dataProvider: TimelineFlameChartDataProvider, event: Common.EventTarget.EventTargetEvent<number>): void { const entryIndex = event.data; if (entryIndex === -1) { this.#minimapComponent.clearBoundsHighlight(); return; } const traceEvent = dataProvider.eventByIndex(entryIndex); if (!traceEvent) { return; } const bounds = Trace.Helpers.Timing.traceWindowFromEvent(traceEvent); this.#minimapComponent.highlightBounds(bounds, /* withBracket */ false); } private loadFromCpuProfile(profile: Protocol.Profiler.Profile|null): void { if (this.state !== State.IDLE || profile === null) { return; } this.prepareToLoadTimeline(); this.loader = TimelineLoader.loadFromCpuProfile(profile, this); } private setState(state: State): void { this.state = state; this.updateTimelineControls(); } /** * This indicates that `this.#setModelForActiveTrace` has been called, * and so the main flame chart should have been populated. */ hasFinishedLoadingTraceForTest(): boolean { return this.#viewMode.mode === 'VIEWING_TRACE'; } private createSettingCheckbox(setting: Common.Settings.Setting<boolean>, tooltip: Platform.UIString.LocalizedString): UI.Toolbar.ToolbarSettingCheckbox { const checkboxItem = new UI.Toolbar.ToolbarSettingCheckbox(setting, tooltip); this.recordingOptionUIControls.push(checkboxItem); return checkboxItem; } #addSidebarIconToToolbar(): void { if (this.panelToolbar.hasItem(this.#sidebarToggleButton)) { return; } this.panelToolbar.prependToolbarItem(this.#sidebarToggleButton); } /** * Used when the user deletes their last trace and is taken back to the * landing page - we don't add this icon until there is a trace loaded. */ #removeSidebarIconFromToolbar(): void { this.panelToolbar.removeToolbarItem(this.#sidebarToggleButton); } #populateDownloadMenu(contextMenu: UI.ContextMenu.ContextMenu): void { // If the current trace is annotated, add an option to save it without annotations. const currModificationManager = ModificationsManager.activeManager(); const annotationsExist = currModificationManager && currModificationManager.getAnnotations()?.length > 0; contextMenu.viewSection().appendItem(i18nString(UIStrings.saveTraceWithAnnotationsMenuOption), () => { Host.userMetrics.actionTaken(Host.UserMetrics.Action.PerfPanelTraceExported); void this.saveToFile({savingEnhancedTrace: false, addModifications: true}); }, { jslogContext: annotationsExist ? 'timeline.save-to-file-with-annotations' : 'timeline.save-to-file-without-annotations', }); if (annotationsExist) { contextMenu.viewSection().appendItem(i18nString(UIStrings.saveTraceWithoutAnnotationsMenuOption), () => { Host.userMetrics.actionTaken(Host.UserMetrics.Action.PerfPanelTraceExported); void this.saveToFile({ savingEnhancedTrace: false, addModifications: false, }); }, { jslogContext: 'timeline.save-to-file-without-annotations', }); } } private populateToolbar(): void { // Record this.panelToolbar.appendToolbarItem(UI.Toolbar.Toolbar.createActionButton(this.toggleRecordAction)); this.panelToolbar.appendToolbarItem(UI.Toolbar.Toolbar.createActionButton(this.recordReloadAction)); this.clearButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.clear), 'clear', undefined, 'timeline.clear'); this.clearButton.addEventListener(UI.Toolbar.ToolbarButton.Events.CLICK, () => this.onClearButton()); this.panelToolbar.appendToolbarItem(this.clearButton); // Load / SaveCLICK this.loadButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.loadProfile), 'import', undefined, 'timeline.load-from-file'); this.loadButton.addEventListener(UI.Toolbar.ToolbarButton.Events.CLICK, () => { Host.userMetrics.actionTaken(Host.UserMetrics.Action.PerfPanelTraceImported); this.selectFileToLoad(); }); this.saveButton = new UI.Toolbar.ToolbarMenuButton( this.#populateDownloadMenu.bind(this), true, false, 'timeline.save-to-file-more-options', 'download'); this.saveButton.setTitle(i18nString(UIStrings.saveProfile)); if (Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.TIMELINE_ENHANCED_TRACES)) { this.saveButton.element.addEventListener('contextmenu', event => { event.preventDefault(); event.stopPropagation(); if (event.ctrlKey || event.button === 2) { const contextMenu = new UI.ContextMenu.ContextMenu(event); contextMenu.saveSection().appendItem(i18nString(UIStrings.exportNormalTraces), () => { void this.saveToFile({savingEnhancedTrace: false, addModifications: false}); }); contextMenu.saveSection().appendItem(i18nString(UIStrings.exportEnhancedTraces), () => { void this.saveToFile({savingEnhancedTrace: true, addModifications: false}); }); void contextMenu.show(); } else { void this.saveToFile({savingEnhancedTrace: false, addModifications: false}); } }); } this.panelToolbar.appendSeparator(); this.panelToolbar.appendToolbarItem(this.loadButton); this.panelToolbar.appendToolbarItem(this.saveButton); // History this.panelToolbar.appendSeparator(); if (!isNode) { this.homeButton = new UI.Toolbar.ToolbarButton( i18nString(UIStrings.backToLiveMetrics), 'home', undefined, 'timeline.back-to-live-metrics'); this.homeButton.addEventListener(UI.Toolbar.ToolbarButton.Events.CLICK, () => { this.#changeView({mode: 'LANDING_PAGE'}); this.#historyManager.navigateToLandingPage(); }); this.panelToolbar.appendToolbarItem(this.homeButton); this.panelToolbar.appendSeparator(); } this.panelToolbar.appendToolbarItem(this.#historyManager.button()); this.panelToolbar.appendSeparator(); // View this.panelToolbar.appendSeparator(); if (!isNode) { this.showScreenshotsToolbarCheckbox = this.createSettingCheckbox(this.showScreenshotsSetting, i18nString(UIStrings.captureScreenshots)); this.panelToolbar.appendToolbarItem(this.showScreenshotsToolbarCheckbox); } this.showMemoryToolbarCheckbox = this.createSettingCheckbox(this.showMemorySetting, i18nString(UIStrings.showMemoryTimeline)); this.panelToolbar.appendToolbarItem(this.showMemoryToolbarCheckbox); // GC this.panelToolbar.appendToolbarItem(UI.Toolbar.Toolbar.createActionButton('components.collect-garbage')); // Ignore list setting this.panelToolbar.appendSeparator(); const showIgnoreListSetting = new TimelineComponents.IgnoreListSetting.IgnoreListSetting(); this.panelToolbar.appendToolbarItem(new UI.Toolbar.ToolbarItem(showIgnoreListSetting)); if (this.#dimThirdPartiesSetting) { const dimThirdPartiesCheckbox = this.createSettingCheckbox(this.#dimThirdPartiesSetting, i18nString(UIStrings.thirdPartiesByThirdPartyWeb)); this.#thirdPartyCheckbox = dimThirdPartiesCheckbox; this.panelToolbar.appendToolbarItem(dimThirdPartiesCheckbox); } // Isolate selector if (isNode) { const isolateSelector = new IsolateSelector(); this.panelToolbar.appendSeparator(); this.panelToolbar.appendToolbarItem(isolateSelector); } // Settings if (!isNode) { this.panelRightToolbar.appendSeparator(); this.panelRightToolbar.appendToolbarItem(this.showSettingsPaneButton); } } #setupNavigationSetting(): HTMLElement { const currentNavSetting = Common.Settings.moduleSetting('flamechart-selected-navigation').get(); const hideTheDialogForTests: string|null = localStorage.getItem('hide-shortcuts-dialog-for-test'); const userHadShortcutsDialogOpenedOnce = this.#userHadShortcutsDialogOpenedOnce.get(); this.#shortcutsDialog.prependElement(this.#navigationRadioButtons); // Add the shortcuts dialog button to the toolbar. const dialogToolbarItem = new UI.Toolbar.ToolbarItem(this.#shortcutsDialog); dialogToolbarItem.element.setAttribute( 'jslog', `${VisualLogging.action().track({click: true}).context('timeline.shortcuts-dialog-toggle')}`); this.panelRightToolbar.appendToolbarItem(dialogToolbarItem); this.#updateNavigationSettingSelection(); // The setting could have been changed from the Devtools Settings. Therefore, we // need to update the radio buttons selection when the dialog is open. this.#shortcutsDialog.addEventListener('click', this.#updateNavigationSettingSelection.bind(this)); this.#shortcutsDialog.data = { customTitle: i18nString(UIStrings.shortcutsDialogTitle), shortcuts: this.#getShortcut