chrome-devtools-frontend
Version:
Chrome DevTools UI
1,208 lines (1,085 loc) • 67 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 * 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 * as TimelineModel from '../../models/timeline_model/timeline_model.js';
import * as TraceEngine from '../../models/trace/trace.js';
import * as PanelFeedback from '../../ui/components/panel_feedback/panel_feedback.js';
import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as MobileThrottling from '../mobile_throttling/mobile_throttling.js';
import historyToolbarButtonStyles from './historyToolbarButton.css.js';
import timelinePanelStyles from './timelinePanel.css.js';
import timelineStatusDialogStyles from './timelineStatusDialog.css.js';
import {Events, PerformanceModel, type WindowChangedEvent} from './PerformanceModel.js';
import {TimelineController, type Client} from './TimelineController.js';
import {
TimelineEventOverviewCPUActivity,
TimelineEventOverviewMemory,
TimelineEventOverviewNetwork,
TimelineEventOverviewResponsiveness,
TimelineFilmStripOverview,
type TimelineEventOverview,
} from './TimelineEventOverview.js';
import {TimelineFlameChartView} from './TimelineFlameChartView.js';
import {TimelineHistoryManager} from './TimelineHistoryManager.js';
import {TimelineLoader} from './TimelineLoader.js';
import {TimelineUIUtils} from './TimelineUIUtils.js';
import {UIDevtoolsController} from './UIDevtoolsController.js';
import {UIDevtoolsUtils} from './UIDevtoolsUtils.js';
import type * as Protocol from '../../generated/protocol.js';
import {traceJsonGenerator} from './SaveFileFormatter.js';
import {TimelineSelection} from './TimelineSelection.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 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 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 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
*/
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
*/
HardwareConcurrencyIsEnabled: '- Hardware concurrency override 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
*/
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 to close something
*/
close: 'Close',
/**
*@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 for an option to learn more about something
*/
learnmore: 'Learn more',
/**
*@description Text in Timeline Panel of the Performance panel
*/
wasd: 'WASD',
/**
*@description Text in Timeline Panel of the Performance panel
*@example {record} PH1
*@example {Ctrl + R} PH2
*/
clickTheRecordButtonSOrHitSTo: 'Click the record button {PH1} or hit {PH2} to start a new recording.',
/**
* @description Text in Timeline Panel of the Performance panel
* @example {reload button} PH1
* @example {Ctrl + R} PH2
*/
clickTheReloadButtonSOrHitSTo: 'Click the reload button {PH1} or hit {PH2} to record the page load.',
/**
*@description Text in Timeline Panel of the Performance panel
*@example {Ctrl + U} PH1
*@example {Learn more} PH2
*/
afterRecordingSelectAnAreaOf:
'After recording, select an area of interest in the overview by dragging. Then, zoom and pan the timeline with the mousewheel or {PH1} keys. {PH2}',
/**
*@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 Time text content in Timeline Panel of the Performance panel
*@example {2.12} PH1
*/
ssec: '{PH1} sec',
/**
*@description Message shown when a browser recording could not be started.
*/
couldNotStart: 'Could not start recording, please try again later',
};
const str_ = i18n.i18n.registerUIStrings('panels/timeline/TimelinePanel.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
let timelinePanelInstance: TimelinePanel;
let isNode: boolean;
// TypeScript will presumably get these types at some stage, and when it
// does these temporary types should be removed.
// TODO: Remove types when available in TypeScript.
declare global {
interface FileSystemWritableFileStream extends WritableStream {
write(data: unknown): Promise<void>;
close(): Promise<void>;
}
interface FileSystemHandle {
createWritable(): Promise<FileSystemWritableFileStream>;
}
interface Window {
showSaveFilePicker(opts: unknown): Promise<FileSystemHandle>;
}
}
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;
private readonly historyManager: TimelineHistoryManager;
private performanceModel: PerformanceModel|null;
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private disableCaptureJSProfileSetting: Common.Settings.Setting<any>;
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private readonly captureLayersAndPicturesSetting: Common.Settings.Setting<any>;
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private showScreenshotsSetting: Common.Settings.Setting<any>;
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private showMemorySetting: Common.Settings.Setting<any>;
private readonly panelToolbar: UI.Toolbar.Toolbar;
private readonly panelRightToolbar: UI.Toolbar.Toolbar;
private readonly timelinePane: UI.Widget.VBox;
private readonly overviewPane: PerfUI.TimelineOverviewPane.TimelineOverviewPane;
private overviewControls: TimelineEventOverview[];
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!: UI.Widget.Widget;
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;
private statusPane!: StatusPane|null;
private landingPage!: UI.Widget.Widget;
private loader?: TimelineLoader;
private showScreenshotsToolbarCheckbox?: UI.Toolbar.ToolbarItem;
private showMemoryToolbarCheckbox?: UI.Toolbar.ToolbarItem;
private networkThrottlingSelect?: UI.Toolbar.ToolbarComboBox;
private cpuThrottlingSelect?: UI.Toolbar.ToolbarComboBox;
private fileSelectorElement?: HTMLInputElement;
private selection?: TimelineSelection|null;
private primaryPageTargetPromiseCallback = (_target: SDK.Target.Target): void => {};
private primaryPageTargetPromise = new Promise<SDK.Target.Target>(res => {
this.primaryPageTargetPromiseCallback = res;
});
#traceEngineModel: TraceEngine.TraceModel.Model<typeof TraceEngine.TraceModel.ENABLED_TRACE_HANDLERS>;
// Tracks the index of the trace that the user is currently viewing.
#traceEngineActiveTraceIndex = -1;
constructor() {
super('timeline');
this.#traceEngineModel = TraceEngine.TraceModel.Model.createWithRequiredHandlersForMigration();
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().action('timeline.toggle-recording') as
UI.ActionRegistration.Action);
this.recordReloadAction =
(UI.ActionRegistry.ActionRegistry.instance().action('timeline.record-reload') as UI.ActionRegistration.Action);
this.historyManager = new TimelineHistoryManager();
this.performanceModel = null;
this.disableCaptureJSProfileSetting =
Common.Settings.Settings.instance().createSetting('timelineDisableJSSampling', false);
this.disableCaptureJSProfileSetting.setTitle(i18nString(UIStrings.disableJavascriptSamples));
this.captureLayersAndPicturesSetting =
Common.Settings.Settings.instance().createSetting('timelineCaptureLayersAndPictures', false);
this.captureLayersAndPicturesSetting.setTitle(i18nString(UIStrings.enableAdvancedPaint));
this.showScreenshotsSetting =
Common.Settings.Settings.instance().createSetting('timelineShowScreenshots', isNode ? false : true);
this.showScreenshotsSetting.setTitle(i18nString(UIStrings.screenshots));
this.showScreenshotsSetting.addChangeListener(this.updateOverviewControls, this);
this.showMemorySetting = Common.Settings.Settings.instance().createSetting('timelineShowMemory', false);
this.showMemorySetting.setTitle(i18nString(UIStrings.memory));
this.showMemorySetting.addChangeListener(this.onModeChanged, this);
const timelineToolbarContainer = this.element.createChild('div', 'timeline-toolbar-container');
this.panelToolbar = new UI.Toolbar.Toolbar('timeline-main-toolbar', timelineToolbarContainer);
this.panelToolbar.makeWrappable(true);
this.panelRightToolbar = new UI.Toolbar.Toolbar('', timelineToolbarContainer);
if (!isNode) {
this.createSettingsPane();
this.updateShowSettingsToolbarButton();
}
this.timelinePane = new UI.Widget.VBox();
this.timelinePane.show(this.element);
const topPaneElement = this.timelinePane.element.createChild('div', 'hbox');
topPaneElement.id = 'timeline-overview-panel';
// Create top overview component.
this.overviewPane = new PerfUI.TimelineOverviewPane.TimelineOverviewPane('timeline');
this.overviewPane.addEventListener(
PerfUI.TimelineOverviewPane.Events.WindowChanged, this.onOverviewWindowChanged.bind(this));
this.overviewPane.show(topPaneElement);
this.overviewControls = [];
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.searchableViewInternal = new UI.SearchableView.SearchableView(this.flameChart, null);
this.searchableViewInternal.setMinimumSize(0, 100);
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.onModeChanged();
this.populateToolbar();
this.showLandingPage();
this.updateTimelineControls();
SDK.TargetManager.TargetManager.instance().addEventListener(
SDK.TargetManager.Events.SuspendStateChanged, this.onSuspendStateChanged, this);
if (Root.Runtime.experiments.isEnabled('timelineAsConsoleProfileResultPanel')) {
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().addModelListener(
SDK.CPUProfilerModel.CPUProfilerModel, SDK.CPUProfilerModel.Events.ConsoleProfileFinished,
event => this.consoleProfileFinished(event.data), this);
}
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) => {},
});
}
static instance(opts: {
forceNew: boolean|null,
isNode: boolean,
}|undefined = {forceNew: null, isNode: false}): TimelinePanel {
const {forceNew, isNode: isNodeMode} = opts;
isNode = isNodeMode;
if (!timelinePanelInstance || forceNew) {
timelinePanelInstance = new TimelinePanel();
}
return timelinePanelInstance;
}
override searchableView(): UI.SearchableView.SearchableView|null {
return this.searchableViewInternal;
}
override wasShown(): void {
super.wasShown();
UI.Context.Context.instance().setFlavor(TimelinePanel, this);
this.registerCSSFiles([timelinePanelStyles]);
// Record the performance tool load time.
Host.userMetrics.panelLoaded('timeline', 'DevTools.Launch.Timeline');
}
override willHide(): void {
UI.Context.Context.instance().setFlavor(TimelinePanel, null);
this.historyManager.cancelIfShowing();
}
loadFromEvents(events: SDK.TracingManager.EventPayload[]): void {
if (this.state !== State.Idle) {
return;
}
this.prepareToLoadTimeline();
this.loader = TimelineLoader.loadFromEvents(events, this);
}
private loadFromCpuProfile(profile: Protocol.Profiler.Profile|null, title?: string): void {
if (this.state !== State.Idle) {
return;
}
this.prepareToLoadTimeline();
this.loader = TimelineLoader.loadFromCpuProfile(profile, this, title);
}
private onOverviewWindowChanged(
event: Common.EventTarget.EventTargetEvent<PerfUI.TimelineOverviewPane.WindowChangedEvent>): void {
if (!this.performanceModel) {
return;
}
const left = event.data.startTime;
const right = event.data.endTime;
this.performanceModel.setWindow({left, right}, /* animate */ true);
}
private onModelWindowChanged(event: Common.EventTarget.EventTargetEvent<WindowChangedEvent>): void {
const window = event.data.window;
this.overviewPane.setWindowTimes(window.left, window.right);
}
private setState(state: State): void {
this.state = state;
this.updateTimelineControls();
}
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private createSettingCheckbox(setting: Common.Settings.Setting<any>, tooltip: string): UI.Toolbar.ToolbarItem {
const checkboxItem = new UI.Toolbar.ToolbarSettingCheckbox(setting, tooltip);
this.recordingOptionUIControls.push(checkboxItem);
return checkboxItem;
}
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');
this.clearButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, () => this.onClearButton());
this.panelToolbar.appendToolbarItem(this.clearButton);
// Load / Save
this.loadButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.loadProfile), 'import');
this.loadButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, () => {
Host.userMetrics.actionTaken(Host.UserMetrics.Action.PerfPanelTraceImported);
this.selectFileToLoad();
});
this.saveButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.saveProfile), 'download');
this.saveButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, _event => {
Host.userMetrics.actionTaken(Host.UserMetrics.Action.PerfPanelTraceExported);
void this.saveToFile();
});
this.panelToolbar.appendSeparator();
this.panelToolbar.appendToolbarItem(this.loadButton);
this.panelToolbar.appendToolbarItem(this.saveButton);
// History
this.panelToolbar.appendSeparator();
this.panelToolbar.appendToolbarItem(this.historyManager.button());
this.panelToolbar.registerCSSFiles([historyToolbarButtonStyles]);
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.createActionButtonForId('components.collect-garbage'));
// Settings
if (!isNode) {
this.panelRightToolbar.appendSeparator();
this.panelRightToolbar.appendToolbarItem(this.showSettingsPaneButton);
}
}
private createSettingsPane(): void {
this.showSettingsPaneSetting =
Common.Settings.Settings.instance().createSetting('timelineShowSettingsToolbar', false);
this.showSettingsPaneButton = new UI.Toolbar.ToolbarSettingToggle(
this.showSettingsPaneSetting, 'gear', i18nString(UIStrings.captureSettings), 'gear-filled');
SDK.NetworkManager.MultitargetNetworkManager.instance().addEventListener(
SDK.NetworkManager.MultitargetNetworkManager.Events.ConditionsChanged, this.updateShowSettingsToolbarButton,
this);
SDK.CPUThrottlingManager.CPUThrottlingManager.instance().addEventListener(
SDK.CPUThrottlingManager.Events.RateChanged, this.updateShowSettingsToolbarButton, this);
SDK.CPUThrottlingManager.CPUThrottlingManager.instance().addEventListener(
SDK.CPUThrottlingManager.Events.HardwareConcurrencyChanged, this.updateShowSettingsToolbarButton, this);
this.disableCaptureJSProfileSetting.addChangeListener(this.updateShowSettingsToolbarButton, this);
this.captureLayersAndPicturesSetting.addChangeListener(this.updateShowSettingsToolbarButton, this);
this.settingsPane = new UI.Widget.HBox();
this.settingsPane.element.classList.add('timeline-settings-pane');
this.settingsPane.show(this.element);
const captureToolbar = new UI.Toolbar.Toolbar('', this.settingsPane.element);
captureToolbar.element.classList.add('flex-auto');
captureToolbar.makeVertical();
captureToolbar.appendToolbarItem(this.createSettingCheckbox(
this.disableCaptureJSProfileSetting, i18nString(UIStrings.disablesJavascriptSampling)));
captureToolbar.appendToolbarItem(
this.createSettingCheckbox(this.captureLayersAndPicturesSetting, i18nString(UIStrings.capturesAdvancedPaint)));
const throttlingPane = new UI.Widget.VBox();
throttlingPane.element.classList.add('flex-auto');
throttlingPane.show(this.settingsPane.element);
const cpuThrottlingToolbar = new UI.Toolbar.Toolbar('', throttlingPane.element);
cpuThrottlingToolbar.appendText(i18nString(UIStrings.cpu));
this.cpuThrottlingSelect = MobileThrottling.ThrottlingManager.throttlingManager().createCPUThrottlingSelector();
cpuThrottlingToolbar.appendToolbarItem(this.cpuThrottlingSelect);
const networkThrottlingToolbar = new UI.Toolbar.Toolbar('', throttlingPane.element);
networkThrottlingToolbar.appendText(i18nString(UIStrings.network));
this.networkThrottlingSelect = this.createNetworkConditionsSelect();
networkThrottlingToolbar.appendToolbarItem(this.networkThrottlingSelect);
const hardwareConcurrencyPane = new UI.Widget.VBox();
hardwareConcurrencyPane.element.classList.add('flex-auto');
hardwareConcurrencyPane.show(this.settingsPane.element);
const {toggle, input, reset, warning} =
MobileThrottling.ThrottlingManager.throttlingManager().createHardwareConcurrencySelector();
const concurrencyThrottlingToolbar = new UI.Toolbar.Toolbar('', hardwareConcurrencyPane.element);
concurrencyThrottlingToolbar.registerCSSFiles([timelinePanelStyles]);
input.element.classList.add('timeline-concurrency-input');
concurrencyThrottlingToolbar.appendToolbarItem(toggle);
concurrencyThrottlingToolbar.appendToolbarItem(input);
concurrencyThrottlingToolbar.appendToolbarItem(reset);
concurrencyThrottlingToolbar.appendToolbarItem(warning);
this.showSettingsPaneSetting.addChangeListener(this.updateSettingsPaneVisibility.bind(this));
this.updateSettingsPaneVisibility();
}
private createNetworkConditionsSelect(): UI.Toolbar.ToolbarComboBox {
const toolbarItem = new UI.Toolbar.ToolbarComboBox(null, i18nString(UIStrings.networkConditions));
toolbarItem.setMaxWidth(140);
MobileThrottling.ThrottlingManager.throttlingManager().decorateSelectWithNetworkThrottling(
toolbarItem.selectElement());
return toolbarItem;
}
private prepareToLoadTimeline(): void {
console.assert(this.state === State.Idle);
this.setState(State.Loading);
if (this.performanceModel) {
this.performanceModel = null;
}
}
private createFileSelector(): void {
if (this.fileSelectorElement) {
this.fileSelectorElement.remove();
}
this.fileSelectorElement = UI.UIUtils.createFileSelectorElement(this.loadFromFile.bind(this));
this.timelinePane.element.appendChild(this.fileSelectorElement);
}
private contextMenu(event: Event): void {
const contextMenu = new UI.ContextMenu.ContextMenu(event);
contextMenu.appendItemsAtLocation('timelineMenu');
void contextMenu.show();
}
async saveToFile(): Promise<void> {
if (this.state !== State.Idle) {
return;
}
const performanceModel = this.performanceModel;
if (!performanceModel) {
return;
}
const traceEvents = this.#traceEngineModel.traceEvents(this.#traceEngineActiveTraceIndex);
const metadata = this.#traceEngineModel.metadata(this.#traceEngineActiveTraceIndex);
if (!traceEvents) {
return;
}
const traceStart = Platform.DateUtilities.toISO8601Compact(new Date());
let fileName: Platform.DevToolsPath.RawPathString;
if (isNode) {
fileName = `CPU-${traceStart}.cpuprofile` as Platform.DevToolsPath.RawPathString;
} else {
fileName = `Trace-${traceStart}.json` as Platform.DevToolsPath.RawPathString;
}
try {
const handler = await window.showSaveFilePicker({
suggestedName: fileName,
});
const encoder = new TextEncoder();
const formattedTraceIter = traceJsonGenerator(traceEvents, metadata);
const traceAsString = Array.from(formattedTraceIter).join('');
const buffer = encoder.encode(traceAsString);
const writable = await handler.createWritable();
await writable.write(buffer);
await writable.close();
} catch (error) {
console.error(error.stack);
if (error.name === 'AbortError') {
// The user cancelled the action, so this is not an error we need to report.
return;
}
Common.Console.Console.instance().error(
i18nString(UIStrings.failedToSaveTimelineSS, {PH1: error.message, PH2: error.name}));
}
}
async showHistory(): Promise<void> {
const recordingData = await this.historyManager.showHistoryDropDown();
if (recordingData && recordingData.legacyModel !== this.performanceModel) {
this.setModel(recordingData.legacyModel, /* exclusiveFilter= */ null, recordingData.traceParseData);
}
}
navigateHistory(direction: number): boolean {
const recordingData = this.historyManager.navigate(direction);
if (recordingData && recordingData.legacyModel !== this.performanceModel) {
this.setModel(recordingData.legacyModel, /* exclusiveFilter= */ null, recordingData.traceParseData);
}
return true;
}
selectFileToLoad(): void {
if (this.fileSelectorElement) {
this.fileSelectorElement.click();
}
}
async loadFromFile(file: File): Promise<void> {
if (this.state !== State.Idle) {
return;
}
this.prepareToLoadTimeline();
this.loader = await TimelineLoader.loadFromFile(file, this);
this.createFileSelector();
}
async loadFromURL(url: Platform.DevToolsPath.UrlString): Promise<void> {
if (this.state !== State.Idle) {
return;
}
this.prepareToLoadTimeline();
this.loader = await TimelineLoader.loadFromURL(url, this);
}
private updateOverviewControls(): void {
this.overviewControls = [];
this.overviewControls.push(new TimelineEventOverviewResponsiveness());
this.overviewControls.push(new TimelineEventOverviewCPUActivity());
this.overviewControls.push(new TimelineEventOverviewNetwork());
if (this.showScreenshotsSetting.get() && this.performanceModel &&
this.performanceModel.filmStripModel().frames().length) {
this.overviewControls.push(new TimelineFilmStripOverview());
}
if (this.showMemorySetting.get()) {
this.overviewControls.push(new TimelineEventOverviewMemory());
}
for (const control of this.overviewControls) {
control.setModel(this.performanceModel);
}
this.overviewPane.setOverviewControls(this.overviewControls);
}
private onModeChanged(): void {
this.updateOverviewControls();
this.doResize();
this.select(null);
}
private updateSettingsPaneVisibility(): void {
if (this.showSettingsPaneSetting.get()) {
this.settingsPane.showWidget();
} else {
this.settingsPane.hideWidget();
}
}
private updateShowSettingsToolbarButton(): void {
const messages: string[] = [];
if (SDK.CPUThrottlingManager.CPUThrottlingManager.instance().cpuThrottlingRate() !== 1) {
messages.push(i18nString(UIStrings.CpuThrottlingIsEnabled));
}
if (MobileThrottling.ThrottlingManager.throttlingManager().hardwareConcurrencyOverrideEnabled) {
messages.push(i18nString(UIStrings.HardwareConcurrencyIsEnabled));
}
if (SDK.NetworkManager.MultitargetNetworkManager.instance().isThrottling()) {
messages.push(i18nString(UIStrings.NetworkThrottlingIsEnabled));
}
if (this.captureLayersAndPicturesSetting.get()) {
messages.push(i18nString(UIStrings.SignificantOverheadDueToPaint));
}
if (this.disableCaptureJSProfileSetting.get()) {
messages.push(i18nString(UIStrings.JavascriptSamplingIsDisabled));
}
this.showSettingsPaneButton.setDefaultWithRedColor(messages.length > 0);
this.showSettingsPaneButton.setToggleWithRedColor(messages.length > 0);
if (messages.length) {
const tooltipElement = document.createElement('div');
messages.forEach(message => {
tooltipElement.createChild('div').textContent = message;
});
this.showSettingsPaneButton.setTitle(tooltipElement.textContent || '');
} else {
this.showSettingsPaneButton.setTitle(i18nString(UIStrings.captureSettings));
}
}
private setUIControlsEnabled(enabled: boolean): void {
this.recordingOptionUIControls.forEach(control => control.setEnabled(enabled));
}
async #evaluateInspectedURL(): Promise<Platform.DevToolsPath.UrlString> {
if (!this.controller) {
return Platform.DevToolsPath.EmptyUrlString;
}
const mainTarget = this.controller.mainTarget();
// target.inspectedURL is reliably populated, however it lacks any url #hash
const inspectedURL = mainTarget.inspectedURL();
// We'll use the navigationHistory to acquire the current URL including hash
const resourceTreeModel = mainTarget.model(SDK.ResourceTreeModel.ResourceTreeModel);
const navHistory = resourceTreeModel && await resourceTreeModel.navigationHistory();
if (!resourceTreeModel || !navHistory) {
return inspectedURL;
}
const {currentIndex, entries} = navHistory;
const navigationEntry = entries[currentIndex];
return navigationEntry.url as Platform.DevToolsPath.UrlString;
}
async #navigateToAboutBlank(): Promise<void> {
const aboutBlankNavigationComplete = new Promise<void>(async (resolve, reject) => {
if (!this.controller) {
reject('Could not find TimelineController');
return;
}
const target = this.controller.mainTarget();
const resourceModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
if (!resourceModel) {
reject('Could not load resourceModel');
return;
}
// To clear out the page and any state from prior test runs, we
// navigate to about:blank before initiating the trace recording.
// Once we have navigated to about:blank, we start recording and
// then navigate to the original page URL, to ensure we profile the
// page load.
function waitForAboutBlank(event: Common.EventTarget.EventTargetEvent<SDK.ResourceTreeModel.ResourceTreeFrame>):
void {
if (event.data.url === 'about:blank') {
resolve();
} else {
reject(`Unexpected navigation to ${event.data.url}`);
}
resourceModel?.removeEventListener(SDK.ResourceTreeModel.Events.FrameNavigated, waitForAboutBlank);
}
resourceModel.addEventListener(SDK.ResourceTreeModel.Events.FrameNavigated, waitForAboutBlank);
await resourceModel.navigate('about:blank' as Platform.DevToolsPath.UrlString);
});
await aboutBlankNavigationComplete;
}
private async startRecording(): Promise<void> {
console.assert(!this.statusPane, 'Status pane is already opened.');
this.setState(State.StartPending);
if (!isNode) {
const recordingOptions = {
enableJSSampling: !this.disableCaptureJSProfileSetting.get(),
capturePictures: this.captureLayersAndPicturesSetting.get(),
captureFilmStrip: this.showScreenshotsSetting.get(),
};
this.showRecordingStarted();
const MAX_WAIT_FOR_TARGET_MS = 2000;
const timeoutPromise = new Promise(res => setTimeout(res, MAX_WAIT_FOR_TARGET_MS));
const primaryPageTarget = await Promise.race([this.primaryPageTargetPromise, timeoutPromise]);
if (!(primaryPageTarget instanceof SDK.Target.Target)) {
this.recordingFailed(i18nString(UIStrings.couldNotStart));
return;
}
if (UIDevtoolsUtils.isUiDevTools()) {
this.controller = new UIDevtoolsController(primaryPageTarget, this);
} else {
this.controller = new TimelineController(primaryPageTarget, this);
}
this.setUIControlsEnabled(false);
this.hideLandingPage();
if (!this.controller) {
throw new Error('Could not create Timeline controller');
}
const urlToTrace = await this.#evaluateInspectedURL();
try {
// If we are doing "Reload & record", we first navigate the page to
// about:blank. This is to ensure any data on the timeline from any
// previous performance recording is lost, avoiding the problem where a
// timeline will show data & screenshots from a previous page load that
// was not relevant.
if (this.recordingPageReload) {
await this.#navigateToAboutBlank();
}
// Order is important here: we tell the controller to start recording, which enables tracing.
const response = await this.controller.startRecording(recordingOptions);
if (response.getError()) {
throw new Error(response.getError());
}
// Once we get here, we know tracing is active.
// This is when, if the user has hit "Reload & Record" that we now need to navigate to the original URL.
// If the user has just hit "record", we don't do any navigating.
const recordingConfig = this.recordingPageReload ? {navigateToUrl: urlToTrace} : undefined;
this.recordingStarted(recordingConfig);
} catch (e) {
this.recordingFailed(e.message);
}
} else {
this.showRecordingStarted();
// Only profile the first target devtools connects to. If we profile all target, but this will cause some bugs
// like time for the function is calculated wrong, because the profiles will be concated and sorted together,
// so the total time will be amplified.
// Multiple targets problem might happen when you inspect multiple node servers on different port at same time,
// or when you let DevTools listen to both locolhost:9229 & 127.0.0.1:9229.
const firstNodeTarget =
SDK.TargetManager.TargetManager.instance().targets().find(target => target.type() === SDK.Target.Type.Node);
if (firstNodeTarget) {
this.cpuProfiler = firstNodeTarget.model(SDK.CPUProfilerModel.CPUProfilerModel);
}
if (this.cpuProfiler) {
this.setUIControlsEnabled(false);
this.hideLandingPage();
await SDK.TargetManager.TargetManager.instance().suspendAllTargets('performance-timeline');
await this.cpuProfiler.startRecording();
this.recordingStarted();
}
}
}
private async stopRecording(): Promise<void> {
if (this.statusPane) {
this.statusPane.finish();
this.statusPane.updateStatus(i18nString(UIStrings.stoppingTimeline));
this.statusPane.updateProgressBar(i18nString(UIStrings.received), 0);
}
this.setState(State.StopPending);
if (this.controller) {
this.performanceModel = this.controller.getPerformanceModel();
await this.controller.stopRecording();
this.setUIControlsEnabled(true);
this.controller.dispose();
this.controller = null;
return;
}
if (this.cpuProfiler) {
const profile = await this.cpuProfiler.stopRecording();
this.setState(State.Idle);
this.loadFromCpuProfile(profile);
this.setUIControlsEnabled(true);
this.cpuProfiler = null;
await SDK.TargetManager.TargetManager.instance().resumeAllTargets();
}
}
private recordingFailed(error: string): void {
if (this.statusPane) {
this.statusPane.remove();
}
this.statusPane = new StatusPane(
{
description: error,
buttonText: i18nString(UIStrings.close),
buttonDisabled: false,
showProgress: undefined,
showTimer: undefined,
},
() => this.loadingComplete(null));
this.statusPane.showPane(this.statusPaneContainer);
this.statusPane.updateStatus(i18nString(UIStrings.recordingFailed));
this.setState(State.RecordingFailed);
this.performanceModel = null;
this.setUIControlsEnabled(true);
if (this.controller) {
this.controller.dispose();
this.controller = null;
}
}
private onSuspendStateChanged(): void {
this.updateTimelineControls();
}
private consoleProfileFinished(data: SDK.CPUProfilerModel.ProfileFinishedData): void {
this.loadFromCpuProfile(data.cpuProfile, data.title);
void UI.InspectorView.InspectorView.instance().showPanel('timeline');
}
private updateTimelineControls(): void {
const state = State;
this.toggleRecordAction.setToggled(this.state === state.Recording);
this.toggleRecordAction.setEnabled(this.state === state.Recording || this.state === state.Idle);
this.recordReloadAction.setEnabled(isNode ? false : this.state === state.Idle);
this.historyManager.setEnabled(this.state === state.Idle);
this.clearButton.setEnabled(this.state === state.Idle);
this.panelToolbar.setEnabled(this.state !== state.Loading);
this.panelRightToolbar.setEnabled(this.state !== state.Loading);
this.dropTarget.setEnabled(this.state === state.Idle);
this.loadButton.setEnabled(this.state === state.Idle);
this.saveButton.setEnabled(this.state === state.Idle && Boolean(this.performanceModel));
}
async toggleRecording(): Promise<void> {
if (this.state === State.Idle) {
this.recordingPageReload = false;
await this.startRecording();
Host.userMetrics.actionTaken(Host.UserMetrics.Action.TimelineStarted);
} else if (this.state === State.Recording) {
await this.stopRecording();
}
}
recordReload(): void {
if (this.state !== State.Idle) {
return;
}
this.recordingPageReload = true;
void this.startRecording();
Host.userMetrics.actionTaken(Host.UserMetrics.Action.TimelinePageReloadStarted);
}
private onClearButton(): void {
this.historyManager.clear();
this.clear();
}
private clear(): void {
this.showLandingPage();
this.reset();
}
private reset(): void {
PerfUI.LineLevelProfile.Performance.instance().reset();
if (this.performanceModel) {
this.performanceModel.removeEventListener(Events.NamesResolved, this.updateModelAndFlameChart, this);
}
this.setModel(null);
}
private applyFilters(
model: PerformanceModel,
exclusiveFilter: TimelineModel.TimelineModelFilter.TimelineModelFilter|null = null): void {
if (model.timelineModel().isGenericTrace() || Root.Runtime.experiments.isEnabled('timelineShowAllEvents')) {
return;
}
model.setFilters(exclusiveFilter ? [exclusiveFilter] : [TimelineUIUtils.visibleEventsFilter()]);
}
private setModel(
model: PerformanceModel|null, exclusiveFilter: TimelineModel.TimelineModelFilter.TimelineModelFilter|null = null,
newTraceEngineData: TraceEngine.TraceModel.PartialTraceParseDataDuringMigration|null = null): void {
if (this.performanceModel) {
this.performanceModel.removeEventListener(Events.WindowChanged, this.onModelWindowChanged, this);
}
this.performanceModel = model;
if (model) {
this.searchableViewInternal.showWidget();
this.applyFilters(model, exclusiveFilter);
} else {
this.searchableViewInternal.hideWidget();
}
this.flameChart.setModel(model, newTraceEngineData);
this.updateOverviewControls();
this.overviewPane.reset();
if (model && this.performanceModel) {
this.performanceModel.addEventListener(Events.WindowChanged, this.onModelWindowChanged, this);
this.overviewPane.setNavStartTimes(model.timelineModel().navStartTimes());
this.overviewPane.setBounds(model.timelineModel().minimumRecordTime(), model.timelineModel().maximumRecordTime());
PerfUI.LineLevelProfile.Performance.instance().reset();
for (const profile of model.timelineModel().cpuProfiles()) {
PerfUI.LineLevelProfile.Performance.instance().appendCPUProfile(profile);
}
this.setMarkers(model.timelineModel());
this.flameChart.setSelection(null);
this.overviewPane.setWindowTimes(model.window().left, model.window().right);
}
for (const control of this.overviewControls) {
control.setModel(model);
}
if (this.flameChart) {
this.flameChart.resizeToPreferredHeights();
}
this.updateTimelineControls();
}
private recordingStarted(config?: {navigateToUrl: Platform.DevToolsPath.UrlString}): void {
if (config && this.recordingPageReload && this.controller) {
// If the user hit "Reload & record", by this point we have:
// 1. Navigated to about:blank
// 2. Initiated tracing.
// We therefore now should navigate back to the original URL that the user wants to profile.
const target = this.controller.mainTarget();
const resourceModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
if (!resourceModel) {
this.recordingFailed('Could not navigate to original URL');
return;
}
// We don't need to await this because we are purposefully showing UI
// progress as the page loads & tracing is underway.
void resourceModel.navigate(config.navigateToUrl);
}
this.reset();
this.setState(State.Recording);
this.showRecordingStarted();
if (this.statusPane) {
this.statusPane.enableAndFocusButton();
this.statusPane.updateStatus(i18nString(UIStrings.profiling));
this.statusPane.updateProgressBar(i18nString(UIStrings.bufferUsage), 0);
this.statusPane.startTimer();
}
this.hideLandingPage();
}
recordingProgress(usage: number): void {
if (this.statusPane) {
this.statusPane.updateProgressBar(i18nString(UIStrings.bufferUsage), usage * 100);
}
}
private showLandingPage(): void {
if (this.landingPage) {
this.landingPage.show(this.statusPaneContainer);
return;
}
function encloseWithTag(tagName: string, contents: string): HTMLElement {
const e = document.createElement(tagName);
e.textContent = contents;
return e;
}
const learnMoreNode = UI.XLink.XLink.create(
'https://developer.chrome.com/docs/devtools/evaluate-performance/', i18nString(UIStrings.learnmore));
const recordKey = encloseWithTag(
'b',
UI.ShortcutRegistry.ShortcutRegistry.instance().shortcutsForAction('timeline.toggle-recording')[0].title());
const reloadKey = encloseWithTag(
'b', UI.ShortcutRegistry.ShortcutRegistry.instance().shortcutsForAction('timeline.record-reload')[0].title());
const navigateNode = encloseWithTag('b', i18nString(UIStrings.wasd));
this.landingPage = new UI.Widget.VBox();
this.landingPage.contentElement.classList.add('timeline-landing-page', 'fill');
const centered = this.landingPage.contentElement.createChild('div');
const recordButton = UI.UIUtils.createInlineButton(UI.Toolbar.Toolbar.createActionButton(this.toggleRecordAction));
const reloadButton =
UI.UIUtils.createInlineButton(UI.Toolbar.Toolbar.createActionButtonForId('timeline.record-reload'));
centered.createChild('p').appendChild(i18n.i18n.getFormatLocalizedString(
str_, UIStrings.clickTheRecordButtonSOrHitSTo, {PH1: recordButton, PH2: recordKey}));
centered.createChild('p').appendChild(i18n.i18n.getFormatLocalizedString(
str_, UIStrings.clickTheReloadButtonSOrHitSTo, {PH1: reloadButton, PH2: reloadKey}));
centered.createChild('p').appendChild(i18n.i18n.getFormatLocalizedString(
str_, UIStrings.afterRecordingSelectAnAreaOf, {PH1: navigateNode, PH2: learnMoreNode}));
if (isNode) {
const previewSection = new PanelFeedback.PanelFeedback.PanelFeedback();
previewSection.data = {
feedbackUrl: 'https://bugs.chromium.org/p/chromium/issues/detail?id=1354548' as Platform.DevToolsPath.UrlString,
quickStartUrl: 'https://developer.chrome.com/blog/js-profiler-deprecation/' as Platform.DevToolsPath.UrlString,
quickStartLinkText: i18nString(UIStrings.learnmore),
};
centered.appendChild(previewSection);
const feedbackButton = new PanelFeedback.FeedbackButton.FeedbackButton();
feedbackButton.data = {
feedbackUrl: 'https://bugs.chromium.org/p/chromium/issues/detail?id=1354548' as Platform.DevToolsPath.UrlString,
};
centered.appendChild(feedbackButton);
}
this.landingPage.show(this.statusPaneContainer);
}
private hideLandingPage(): void {
this.landingPage.detach();
}
async loadingStarted(): Promise<void> {
this.hideLandingPage();
if (this.statusPane) {
this.statusPane.remove();
}
this.statusPane = new StatusPane(
{
showProgress: true,
showTimer: undefined,
buttonDisabled: undefined,
buttonText: undefined,
description: undefined,
},
() => this.cancelLoading());
this.statusPane.showPane(this.statusPaneCon