chrome-devtools-frontend
Version:
Chrome DevTools UI
1,163 lines (1,044 loc) • 123 kB
text/typescript
// 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