UNPKG

chrome-devtools-frontend

Version:
1,261 lines (1,131 loc) 60.2 kB
// Copyright 2021 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /* * 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 Bindings from '../../models/bindings/bindings.js'; import * as Extensions from '../../models/extensions/extensions.js'; import * as TimelineModel from '../../models/timeline_model/timeline_model.js'; import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js'; import * as UI from '../../ui/legacy/legacy.js'; import historyToolbarButtonStyles from './historyToolbarButton.css.js'; import timelinePanelStyles from './timelinePanel.css.js'; import timelineStatusDialogStyles from './timelineStatusDialog.css.js'; import type * as Coverage from '../coverage/coverage.js'; import * as MobileThrottling from '../mobile_throttling/mobile_throttling.js'; import type {WindowChangedEvent} from './PerformanceModel.js'; import {Events, PerformanceModel} from './PerformanceModel.js'; import type {Client} from './TimelineController.js'; import {TimelineController} from './TimelineController.js'; import type {TimelineEventOverview} from './TimelineEventOverview.js'; import { TimelineEventOverviewCoverage, TimelineEventOverviewCPUActivity, TimelineEventOverviewInput, TimelineEventOverviewMemory, TimelineEventOverviewNetwork, TimelineEventOverviewResponsiveness, TimelineFilmStripOverview, } 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'; 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 Title of the 'Coverage' tool in the bottom drawer */ coverage: 'Coverage', /** *@description Text for the memory of the page */ memory: 'Memory', /** *@description Text in Timeline for the Web Vitals lane */ webVitals: 'Web Vitals', /** *@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 Text in Timeline for the Web Vitals lane checkbox */ showWebVitals: 'Show Web Vitals', /** *@description Text in Timeline Panel of the Performance panel */ recordCoverageWithPerformance: 'Record coverage with performance trace', /** *@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 *@example {2} PH3 */ failedToSaveTimelineSSS: 'Failed to save timeline: {PH1} ({PH2}, {PH3})', /** *@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', }; const str_ = i18n.i18n.registerUIStrings('panels/timeline/TimelinePanel.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); let timelinePanelInstance: TimelinePanel; 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 readonly viewModeSetting: 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 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 startCoverage: 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>; // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // eslint-disable-next-line @typescript-eslint/no-explicit-any private showWebVitalsSetting: 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 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 showWebVitalsToolbarCheckbox?: UI.Toolbar.ToolbarItem; private startCoverageCheckbox?: UI.Toolbar.ToolbarItem; private networkThrottlingSelect?: UI.Toolbar.ToolbarComboBox; private cpuThrottlingSelect?: UI.Toolbar.ToolbarComboBox; private fileSelectorElement?: HTMLInputElement; private selection?: TimelineSelection|null; constructor() { super('timeline'); 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.viewModeSetting = Common.Settings.Settings.instance().createSetting('timelineViewMode', ViewMode.FlameChart); 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', true); this.showScreenshotsSetting.setTitle(i18nString(UIStrings.screenshots)); this.showScreenshotsSetting.addChangeListener(this.updateOverviewControls, this); this.startCoverage = Common.Settings.Settings.instance().createSetting('timelineStartCoverage', false); this.startCoverage.setTitle(i18nString(UIStrings.coverage)); if (!Root.Runtime.experiments.isEnabled('recordCoverageWithPerformanceTracing')) { this.startCoverage.set(false); } this.showMemorySetting = Common.Settings.Settings.instance().createSetting('timelineShowMemory', false); this.showMemorySetting.setTitle(i18nString(UIStrings.memory)); this.showMemorySetting.addChangeListener(this.onModeChanged, this); this.showWebVitalsSetting = Common.Settings.Settings.instance().createSetting('timelineWebVitals', false); this.showWebVitalsSetting.setTitle(i18nString(UIStrings.webVitals)); this.showWebVitalsSetting.addChangeListener(this.onWebVitalsChanged, 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); 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.onWebVitalsChanged(); this.populateToolbar(); this.showLandingPage(); this.updateTimelineControls(); Extensions.ExtensionServer.ExtensionServer.instance().addEventListener( Extensions.ExtensionServer.Events.TraceProviderAdded, this.appendExtensionsToToolbar, this); SDK.TargetManager.TargetManager.instance().addEventListener( SDK.TargetManager.Events.SuspendStateChanged, this.onSuspendStateChanged, this); } static instance(opts: { forceNew: boolean|null, }|undefined = {forceNew: null}): TimelinePanel { const {forceNew} = opts; if (!timelinePanelInstance || forceNew) { timelinePanelInstance = new TimelinePanel(); } return timelinePanelInstance; } searchableView(): UI.SearchableView.SearchableView|null { return this.searchableViewInternal; } 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'); } 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 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), 'largeicon-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), 'largeicon-load'); 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), 'largeicon-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(); 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); this.showWebVitalsToolbarCheckbox = this.createSettingCheckbox(this.showWebVitalsSetting, i18nString(UIStrings.showWebVitals)); this.panelToolbar.appendToolbarItem(this.showWebVitalsToolbarCheckbox); if (Root.Runtime.experiments.isEnabled('recordCoverageWithPerformanceTracing')) { this.startCoverageCheckbox = this.createSettingCheckbox(this.startCoverage, i18nString(UIStrings.recordCoverageWithPerformance)); this.panelToolbar.appendToolbarItem(this.startCoverageCheckbox); } // GC this.panelToolbar.appendToolbarItem(UI.Toolbar.Toolbar.createActionButtonForId('components.collect-garbage')); // Settings 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, 'largeicon-settings-gear', i18nString(UIStrings.captureSettings)); 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 appendExtensionsToToolbar( event: Common.EventTarget.EventTargetEvent<Extensions.ExtensionTraceProvider.ExtensionTraceProvider>): void { const provider = event.data; const setting = TimelinePanel.settingForTraceProvider(provider); const checkbox = this.createSettingCheckbox(setting, provider.longDisplayName()); this.panelToolbar.appendToolbarItem(checkbox); } private static settingForTraceProvider(traceProvider: Extensions.ExtensionTraceProvider.ExtensionTraceProvider): Common.Settings.Setting<boolean> { let setting = traceProviderToSetting.get(traceProvider); if (!setting) { const providerId = traceProvider.persistentIdentifier(); setting = Common.Settings.Settings.instance().createSetting(providerId, false); setting.setTitle(traceProvider.shortDisplayName()); traceProviderToSetting.set(traceProvider, setting); } return setting; } 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.dispose(); 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 now = new Date(); const fileName = 'Profile-' + Platform.DateUtilities.toISO8601Compact(now) + '.json' as Platform.DevToolsPath.RawPathString; const stream = new Bindings.FileUtils.FileOutputStream(); const accepted = await stream.open(fileName); if (!accepted) { return; } const error = (await performanceModel.save(stream) as { message: string, name: string, code: number, } | null); if (!error) { return; } Common.Console.Console.instance().error( i18nString(UIStrings.failedToSaveTimelineSSS, {PH1: error.message, PH2: error.name, PH3: error.code})); } async showHistory(): Promise<void> { const model = await this.historyManager.showHistoryDropDown(); if (model && model !== this.performanceModel) { this.setModel(model); } } navigateHistory(direction: number): boolean { const model = this.historyManager.navigate(direction); if (model && model !== this.performanceModel) { this.setModel(model); } return true; } selectFileToLoad(): void { if (this.fileSelectorElement) { this.fileSelectorElement.click(); } } private loadFromFile(file: File): void { if (this.state !== State.Idle) { return; } this.prepareToLoadTimeline(); this.loader = TimelineLoader.loadFromFile(file, this); this.createFileSelector(); } loadFromURL(url: Platform.DevToolsPath.UrlString): void { if (this.state !== State.Idle) { return; } this.prepareToLoadTimeline(); this.loader = TimelineLoader.loadFromURL(url, this); } private updateOverviewControls(): void { this.overviewControls = []; this.overviewControls.push(new TimelineEventOverviewResponsiveness()); if (Root.Runtime.experiments.isEnabled('inputEventsOnTimelineOverview')) { this.overviewControls.push(new TimelineEventOverviewInput()); } 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()); } if (this.startCoverage.get()) { this.overviewControls.push(new TimelineEventOverviewCoverage()); } 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 onWebVitalsChanged(): void { this.flameChart.toggleWebVitalsLane(); } 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)); } private async getCoverageViewWidget(): Promise<Coverage.CoverageView.CoverageView> { const view = UI.ViewManager.ViewManager.instance().view('coverage'); return await view.widget() as Coverage.CoverageView.CoverageView; } private async startRecording(): Promise<void> { console.assert(!this.statusPane, 'Status pane is already opened.'); this.setState(State.StartPending); const recordingOptions = { enableJSSampling: !this.disableCaptureJSProfileSetting.get(), capturePictures: this.captureLayersAndPicturesSetting.get(), captureFilmStrip: this.showScreenshotsSetting.get(), startCoverage: this.startCoverage.get(), }; if (recordingOptions.startCoverage) { await UI.ViewManager.ViewManager.instance() .showView('coverage') .then(() => this.getCoverageViewWidget()) .then(widget => widget.ensureRecordingStarted()); } this.showRecordingStarted(); const enabledTraceProviders = Extensions.ExtensionServer.ExtensionServer.instance().traceProviders().filter( provider => TimelinePanel.settingForTraceProvider(provider).get()); const mainTarget = (SDK.TargetManager.TargetManager.instance().mainTarget() as SDK.Target.Target); if (UIDevtoolsUtils.isUiDevTools()) { this.controller = new UIDevtoolsController(mainTarget, this); } else { this.controller = new TimelineController(mainTarget, this); } this.setUIControlsEnabled(false); this.hideLandingPage(); try { const response = await this.controller.startRecording(recordingOptions, enabledTraceProviders); if (response.getError()) { throw new Error(response.getError()); } else { this.recordingStarted(); } } catch (e) { this.recordingFailed(e.message); } } 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.startCoverage.get()) { await UI.ViewManager.ViewManager.instance() .showView('coverage') .then(() => this.getCoverageViewWidget()) .then(widget => widget.stopRecording()); } if (this.controller) { const model = await this.controller.stopRecording(); this.performanceModel = model; this.setUIControlsEnabled(true); this.controller.dispose(); this.controller = null; } } 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 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(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)); } toggleRecording(): void { if (this.state === State.Idle) { this.recordingPageReload = false; void this.startRecording(); Host.userMetrics.actionTaken(Host.UserMetrics.Action.TimelineStarted); } else if (this.state === State.Recording) { void 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(); this.setModel(null); } private applyFilters(model: PerformanceModel): void { if (model.timelineModel().isGenericTrace() || Root.Runtime.experiments.isEnabled('timelineShowAllEvents')) { return; } model.setFilters([TimelineUIUtils.visibleEventsFilter()]); } private setModel(model: PerformanceModel|null): void { if (this.performanceModel) { this.performanceModel.removeEventListener(Events.WindowChanged, this.onModelWindowChanged, this); } this.performanceModel = model; if (model) { this.searchableViewInternal.showWidget(); this.applyFilters(model); } else { this.searchableViewInternal.hideWidget(); } this.flameChart.setModel(model); 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(): void { if (this.recordingPageReload && this.controller) { const target = this.controller.mainTarget(); const resourceModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel); if (resourceModel) { resourceModel.reloadPage(); } } 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})); this.landingPage.show(this.statusPaneContainer); } private hideLandingPage(): void { this.landingPage.detach(); } loadingStarted(): 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.statusPaneContainer); this.statusPane.updateStatus(i18nString(UIStrings.loadingProfile)); // FIXME: make loading from backend cancelable as well. if (!this.loader) { this.statusPane.finish(); } this.loadingProgress(0); } loadingProgress(progress?: number): void { if (typeof progress === 'number' && this.statusPane) { this.statusPane.updateProgressBar(i18nString(UIStrings.received), progress * 100); } } processingStarted(): void { if (this.statusPane) { this.statusPane.updateStatus(i18nString(UIStrings.processingProfile)); } } loadingComplete(tracingModel: SDK.TracingModel.TracingModel|null): void { delete this.loader; this.setState(State.Idle); if (this.statusPane) { this.statusPane.remove(); } this.statusPane = null; if (!tracingModel) { this.clear(); return; } if (!this.performanceModel) { this.performanceModel = new PerformanceModel(); } this.performanceModel.setTracingModel(tracingModel); this.setModel(this.performanceModel); this.historyManager.addRecording(this.performanceModel); if (this.startCoverage.get()) { void UI.ViewManager.ViewManager.instance() .showView('coverage') .then(() => this.getCoverageViewWidget()) .then(widget => widget.processBacklog()) .then(() => this.updateOverviewControls()); } } private showRecordingStarted(): void { if (this.statusPane) { return; } this.statusPane = new StatusPane( { showTimer: true, showProgress: true, buttonDisabled: true, description: undefined, buttonText: undefined, }, () => this.stopRecording()); this.statusPane.showPane(this.statusPaneContainer); this.statusPane.updateStatus(i18nString(UIStrings.initializingProfiler)); } private cancelLoading(): void { if (this.loader) { this.loader.cancel(); } } private setMarkers(timelineModel: TimelineModel.TimelineModel.TimelineModelImpl): void { const markers = new Map<number, Element>(); const recordTypes = TimelineModel.TimelineModel.RecordType; const zeroTime = timelineModel.minimumRecordTime(); for (const event of timelineModel.timeMarkerEvents()) { if (event.name === recordTypes.TimeStamp || event.name === recordTypes.ConsoleTime) { continue; } markers.set(event.startTime, TimelineUIUtils.createEventDivider(event, zeroTime)); } // Add markers for navigation start times. for (const navStartTimeEvent of timelineModel.navStartTimes().values()) { markers.set(navStartTimeEvent.startTime, TimelineUIUtils.createEventDivider(navStartTimeEvent, zeroTime)); } this.overviewPane.setMarkers(markers); } private async loadEventFired( event: Common.EventTarget .EventTargetEvent<{resourceTreeModel: SDK.ResourceTreeModel.ResourceTreeModel, loadTime: number}>): Promise<void> { if (this.state !== State.Recording || !this.recordingPageReload || !this.controller || this.controller.mainTarget() !== event.data.resourceTreeModel.target()) { return; } const controller = this.controller; await new Promise(r => window.setTimeout(r, this.millisecondsToRecordAfterLoadEvent)); // Check if we're still in the same recording session. if (controller !== this.controller || this.state !== State.Recording) { return; } void this.stopRecording(); } private frameForSelection(selection: TimelineSelection): TimelineModel.TimelineFrameModel.TimelineFrame|null { switch (selection.type()) { case TimelineSelection.Type.Frame: return selection.object() as TimelineModel.TimelineFrameModel.TimelineFrame; case TimelineSelection.Type.Range: return null; case TimelineSelection.Type.TraceEvent: if (!this.performanceModel) { return null; } return this.performanceModel.frameModel().getFramesWithinWindow( selection.endTimeInternal, selection.endTimeInternal)[0]; default: console.assert(false, 'Should never be reached'); return null; } } jumpToFrame(offset: number): true|undefined { const currentFrame = this.selection && this.frameForSelection(this.selection); if (!currentFrame || !this.performanceModel) { return; } const frames = this.performanceModel.frames(); let index = frames.indexOf(currentFrame); console.assert(index >= 0, 'Can\'t find current frame in the frame list'); index = Platform.NumberUtilities.clamp(index + offset, 0, frames.length - 1); const frame = frames[index]; this.revealTimeRange(frame.startTime, frame.endTime); this.select(TimelineSelection.fromFrame(frame)); return true; } select(selection: TimelineSelection|null): void { this.selection = selection; this.flameChart.setSelection(selection); } selectEntryAtTime(events: SDK.TracingModel.Event[]|null, time: number): void { if (!events) { return; } // Find best match, then backtrack to the first visible entry. for (let index = Platform.ArrayUtilities.upperBound(events, time, (time, event) => time - event.startTime) - 1; index >= 0; --index) { const event = events[index]; const endTime = event.endTime || event.startTime; if (SDK.TracingModel.TracingModel.isTopLevelEvent(event) && endTime < time) { break; } if (this.performanceModel && this.performanceModel.isVisible(event) && endTime >= time) { this.select(TimelineSelection.fromTraceEvent(event)); return; } } this.select(null); } highlightEvent(event: SDK.TracingModel.Event|null): void { this.flameChart.highlightEvent(event); } private revealTimeRange(startTime: number, endTime: number): void { if (!this.performanceModel) { return; } const window = this.performanceModel.window(); let offset = 0; if (window.right < endTime) { offset = endTime - window.right; } else if (window.left >