chrome-devtools-frontend
Version:
Chrome DevTools UI
1,162 lines (1,050 loc) • 119 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.
/*
* 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 type * as Buttons from '../../ui/components/buttons/buttons.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 {SHOULD_SHOW_EASTER_EGG} from './EasterEgg.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 {type Client, TimelineController} from './TimelineController.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,
type TimelineSelection,
} from './TimelineSelection.js';
import timelineStatusDialogStyles from './timelineStatusDialog.css.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 disable capture jsprofile setting in timeline panel of the performance panel
*/
disableJavascriptSamples: 'Disable JavaScript samples',
/**
*@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
*/
disablesJavascriptSampling: 'Disables JavaScript sampling, reduces overhead when running against mobile devices',
/**
*@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
*@example {wrong format} PH1
*@example {ERROR_FILE_NOT_FOUND} PH2
*/
failedToSaveTimelineSS: 'Failed to save timeline: {PH1} ({PH2})',
/**
*@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
*/
JavascriptSamplingIsDisabled: '- JavaScript sampling is disabled',
/**
*@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 Text to download the trace file after an error
*/
downloadAfterError: 'Download trace',
/**
*@description Status text to indicate the recording has failed in the Performance panel
*/
recordingFailed: 'Recording 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 the status of something
*/
status: 'Status',
/**
*@description Text that refers to the time
*/
time: 'Time',
/**
*@description Text for the description of something
*/
description: 'Description',
/**
*@description Text of an item that stops the running task
*/
stop: 'Stop',
/**
*
* @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 up/down scroll action that appears in the Performance panel shortcuts dialog.
*/
timelineScrollUpDown: 'Move up/down',
/**
* @description Description of the Timeline left/right panning action that appears in the Performance panel shortcuts dialog.
*/
timelinePanLeftRight: 'Move left/right',
/**
* @description Description of the Timeline in/out zoom action that appears in the Performance panel shortcuts dialog.
*/
timelineZoomInOut: 'Zoom in/out',
/**
* @description Description of the Timeline fast in/out zoom action that appears in the Performance panel shortcuts dialog.
*/
timelineFastZoomInOut: 'Fast zoom in/out',
/**
* @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',
};
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 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 disableCaptureJSProfileSetting: Common.Settings.Setting<boolean>;
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: Map<number, Trace.Extras.TraceFilter.TraceFilter> = new Map();
/**
* 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 brickBreakerToolbarButton: UI.Toolbar.ToolbarButton;
private brickBreakerToolbarButtonAdded = false;
private loadButton!: UI.Toolbar.ToolbarButton;
private saveButton!: UI.Toolbar.ToolbarButton|UI.Toolbar.ToolbarMenuButton;
private homeButton?: UI.Toolbar.ToolbarButton;
private statusPane!: StatusPane|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;
private primaryPageTargetPromiseCallback = (_target: SDK.Target.Target): void => {};
// Note: this is technically unused, but we need it to define the promiseCallback function above.
private primaryPageTargetPromise = new Promise<SDK.Target.Target>(res => {
this.primaryPageTargetPromiseCallback = res;
});
#traceEngineModel: Trace.TraceModel.Model;
#sourceMapsResolver: Utils.SourceMapsResolver.SourceMapsResolver|null = null;
#entityMapper: Utils.EntityMapper.EntityMapper|null = null;
#onSourceMapsNodeNamesResolvedBound = this.#onSourceMapsNodeNamesResolved.bind(this);
readonly #onChartPlayableStateChangeBound: (event: Common.EventTarget.EventTargetEvent<boolean>) => void;
#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: boolean = 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', 'timeline.select-modern-navigation');
#classicNavRadioButton =
UI.UIUtils.createRadioButton('flamechart-selected-navigation', 'Classic', '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.brickBreakerToolbarButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.fixMe), adorner);
this.brickBreakerToolbarButton.addEventListener(
UI.Toolbar.ToolbarButton.Events.CLICK, () => this.#onBrickBreakerEasterEggClick());
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.disableCaptureJSProfileSetting =
Common.Settings.Settings.instance().createSetting('timeline-disable-js-sampling', false);
this.disableCaptureJSProfileSetting.setTitle(i18nString(UIStrings.disableJavascriptSamples));
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);
this.showMemorySetting.setTitle(i18nString(UIStrings.memory));
this.showMemorySetting.addChangeListener(this.onMemoryModeChanged, this);
this.#dimThirdPartiesSetting =
Common.Settings.Settings.instance().createSetting('timeline-dim-third-parties', false);
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.#onChartPlayableStateChangeBound = this.#onChartPlayableStateChange.bind(this);
this.element.addEventListener(
'toggle-popover', event => this.flameChart.togglePopover((event as CustomEvent).detail));
this.flameChart.getMainFlameChart().addEventListener(
PerfUI.FlameChart.Events.CHART_PLAYABLE_STATE_CHANGED, this.#onChartPlayableStateChangeBound, this);
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});
});
this.#sideBar.element.addEventListener(TimelineInsights.SidebarInsight.InsightProvideOverlays.eventName, event => {
const {overlays, options} = event;
this.flameChart.setOverlays(overlays, options);
const overlaysBounds = Overlays.Overlays.traceWindowContainingOverlays(overlays);
if (overlaysBounds) {
this.#minimapComponent.highlightBounds(overlaysBounds, /* withBracket */ true);
} else {
this.#minimapComponent.clearBoundsHighlight();
}
});
this.flameChart.element.addEventListener(TimelineInsights.EventRef.EventReferenceClick.eventName, event => {
const fromTraceEvent = selectionFromEvent(event.event);
this.flameChart.setSelectionAndReveal(fromTraceEvent);
});
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) => {
},
},
);
SDK.TargetManager.TargetManager.instance().observeTargets({
targetAdded: (target: SDK.Target.Target) => {
if (target !== SDK.TargetManager.TargetManager.instance().primaryPageTarget()) {
return;
}
this.primaryPageTargetPromiseCallback(target);
},
targetRemoved: (_: SDK.Target.Target) => {},
});
}
#setActiveInsight(insight: TimelineComponents.Sidebar.ActiveInsight|null): void {
// When an insight is selected, ensure that the 3P checkbox is disabled
// to avoid dimming interference.
if (insight) {
this.#splitWidget.showBoth();
}
this.#sideBar.setActiveInsight(insight);
this.flameChart.setActiveInsight(insight);
}
/**
* This "disables" the 3P checkbox in the toolbar.
* Disabling here does a couple of things:
* 1) makes the checkbox dimmed and unclickable
* 2) gives the checkbox UI an indeterminate state
*/
set3PCheckboxDisabled(disabled: boolean): void {
if (Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.TIMELINE_DIM_UNRELATED_EVENTS)) {
this.#thirdPartyCheckbox?.applyEnabledState(!disabled);
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);
}
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.statusPane) {
this.statusPane.remove();
}
this.statusPane = null;
}
#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();
// 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();
// Hide the brick-breaker easter egg
this.brickBreakerToolbarButtonAdded = false;
this.panelToolbar.removeToolbarItem(this.brickBreakerToolbarButton);
return;
}
case 'VIEWING_TRACE': {
this.#hideLandingPage();
this.#setModelForActiveTrace();
this.#removeStatusPane();
this.#showSidebarIfRequired();
this.flameChart.dimThirdPartiesIfRequired();
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();
// 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;
}
#onChartPlayableStateChange(event: Common.EventTarget.EventTargetEvent<boolean, unknown>): void {
if (event.data) {
const dateObj = new Date();
const month = dateObj.getUTCMonth() + 1;
const day = dateObj.getUTCDate();
const isAprilFools = (month === 4 && (day === 1 || day === 2)); // Show only on April fools and the next day
if (isAprilFools && !this.brickBreakerToolbarButtonAdded && SHOULD_SHOW_EASTER_EGG) {
this.brickBreakerToolbarButtonAdded = true;
this.panelToolbar.appendToolbarItem(this.brickBreakerToolbarButton);
}
} else {
this.brickBreakerToolbarButtonAdded = false;
this.panelToolbar.removeToolbarItem(this.brickBreakerToolbarButton);
}
}
#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();
}
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 {
contextMenu.viewSection().appendItem(i18nString(UIStrings.saveTraceWithAnnotationsMenuOption), () => {
Host.userMetrics.actionTaken(Host.UserMetrics.Action.PerfPanelTraceExported);
void this.saveToFile(/* isEnhancedTraces */ false, /* addModifications */ true);
}, {
jslogContext: 'timeline.save-to-file-with-annotations',
});
contextMenu.viewSection().appendItem(i18nString(UIStrings.saveTraceWithoutAnnotationsMenuOption), () => {
Host.userMetrics.actionTaken(Host.UserMetrics.Action.PerfPanelTraceExported);
void this.saveToFile();
}, {
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, true, '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();
});
contextMenu.saveSection().appendItem(i18nString(UIStrings.exportEnhancedTraces), () => {
void this.saveToFile(/* isEnhancedTraces */ true);
});
void contextMenu.show();
} else {
void this.saveToFile();
}
});
}
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.#dimThirdP