UNPKG

chrome-devtools-frontend

Version:
1,208 lines (1,085 loc) 67 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 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