| // 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, |
| TimelineEventOverviewFrames, |
| 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 |
| */ |
| 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); |
| 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 networkThrottlingToolbar = new UI.Toolbar.Toolbar('', throttlingPane.element); |
| networkThrottlingToolbar.appendText(i18nString(UIStrings.network)); |
| this.networkThrottlingSelect = this.createNetworkConditionsSelect(); |
| networkThrottlingToolbar.appendToolbarItem(this.networkThrottlingSelect); |
| |
| const cpuThrottlingToolbar = new UI.Toolbar.Toolbar('', throttlingPane.element); |
| cpuThrottlingToolbar.appendText(i18nString(UIStrings.cpu)); |
| this.cpuThrottlingSelect = MobileThrottling.ThrottlingManager.throttlingManager().createCPUThrottlingSelector(); |
| cpuThrottlingToolbar.appendToolbarItem(this.cpuThrottlingSelect); |
| |
| 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: string): 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 TimelineEventOverviewFrames()); |
| 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 (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 > startTime) { |
| offset = startTime - window.left; |
| } |
| this.performanceModel.setWindow({left: window.left + offset, right: window.right + offset}, /* animate */ true); |
| } |
| |
| private handleDrop(dataTransfer: DataTransfer): void { |
| const items = dataTransfer.items; |
| if (!items.length) { |
| return; |
| } |
| const item = items[0]; |
| Host.userMetrics.actionTaken(Host.UserMetrics.Action.PerfPanelTraceImported); |
| if (item.kind === 'string') { |
| const url = dataTransfer.getData('text/uri-list'); |
| if (new Common.ParsedURL.ParsedURL(url).isValid) { |
| this.loadFromURL(url); |
| } |
| } else if (item.kind === 'file') { |
| const file = items[0].getAsFile(); |
| if (!file) { |
| return; |
| } |
| this.loadFromFile(file); |
| } |
| } |
| } |
| |
| // TODO(crbug.com/1167717): Make this a const enum again |
| // eslint-disable-next-line rulesdir/const_enum |
| export enum State { |
| Idle = 'Idle', |
| StartPending = 'StartPending', |
| Recording = 'Recording', |
| StopPending = 'StopPending', |
| Loading = 'Loading', |
| RecordingFailed = 'RecordingFailed', |
| } |
| |
| // TODO(crbug.com/1167717): Make this a const enum again |
| // eslint-disable-next-line rulesdir/const_enum |
| export enum ViewMode { |
| FlameChart = 'FlameChart', |
| BottomUp = 'BottomUp', |
| CallTree = 'CallTree', |
| EventLog = 'EventLog', |
| } |
| |
| // Define row and header height, should be in sync with styles for timeline graphs. |
| export const rowHeight = 18; |
| |
| export const headerHeight = 20; |
| |
| export class TimelineSelection { |
| private readonly typeInternal: string; |
| private readonly startTimeInternal: number; |
| readonly endTimeInternal: number; |
| private readonly objectInternal: Object|null; |
| |
| constructor(type: string, startTime: number, endTime: number, object?: Object) { |
| this.typeInternal = type; |
| this.startTimeInternal = startTime; |
| this.endTimeInternal = endTime; |
| this.objectInternal = object || null; |
| } |
| |
| static fromFrame(frame: TimelineModel.TimelineFrameModel.TimelineFrame): TimelineSelection { |
| return new TimelineSelection(TimelineSelection.Type.Frame, frame.startTime, frame.endTime, frame); |
| } |
| |
| static fromNetworkRequest(request: TimelineModel.TimelineModel.NetworkRequest): TimelineSelection { |
| return new TimelineSelection( |
| TimelineSelection.Type.NetworkRequest, request.startTime, request.endTime || request.startTime, request); |
| } |
| |
| static fromTraceEvent(event: SDK.TracingModel.Event): TimelineSelection { |
| return new TimelineSelection( |
| TimelineSelection.Type.TraceEvent, event.startTime, event.endTime || (event.startTime + 1), event); |
| } |
| |
| static fromRange(startTime: number, endTime: number): TimelineSelection { |
| return new TimelineSelection(TimelineSelection.Type.Range, startTime, endTime); |
| } |
| |
| type(): string { |
| return this.typeInternal; |
| } |
| |
| object(): Object|null { |
| return this.objectInternal; |
| } |
| |
| startTime(): number { |
| return this.startTimeInternal; |
| } |
| |
| endTime(): number { |
| return this.endTimeInternal; |
| } |
| } |
| |
| export namespace TimelineSelection { |
| // TODO(crbug.com/1167717): Make this a const enum again |
| // eslint-disable-next-line rulesdir/const_enum |
| export enum Type { |
| Frame = 'Frame', |
| NetworkRequest = 'NetworkRequest', |
| TraceEvent = 'TraceEvent', |
| Range = 'Range', |
| } |
| } |
| export interface TimelineModeViewDelegate { |
| select(selection: TimelineSelection|null): void; |
| selectEntryAtTime(events: SDK.TracingModel.Event[]|null, time: number): void; |
| highlightEvent(event: SDK.TracingModel.Event|null): void; |
| } |
| |
| export class StatusPane extends UI.Widget.VBox { |
| private status: HTMLElement; |
| private time!: Element; |
| private progressLabel!: Element; |
| private progressBar!: Element; |
| private readonly description: HTMLElement|undefined; |
| private button: HTMLButtonElement; |
| private startTime!: number; |
| private timeUpdateTimer?: number; |
| |
| constructor( |
| options: { |
| showTimer?: boolean, |
| showProgress?: boolean, |
| description?: string, |
| buttonText?: string, |
| buttonDisabled?: boolean, |
| }, |
| buttonCallback: () => (Promise<void>| void)) { |
| super(true); |
| |
| this.contentElement.classList.add('timeline-status-dialog'); |
| |
| const statusLine = this.contentElement.createChild('div', 'status-dialog-line status'); |
| statusLine.createChild('div', 'label').textContent = i18nString(UIStrings.status); |
| this.status = statusLine.createChild('div', 'content'); |
| UI.ARIAUtils.markAsStatus(this.status); |
| |
| if (options.showTimer) { |
| const timeLine = this.contentElement.createChild('div', 'status-dialog-line time'); |
| timeLine.createChild('div', 'label').textContent = i18nString(UIStrings.time); |
| this.time = timeLine.createChild('div', 'content'); |
| } |
| |
| if (options.showProgress) { |
| const progressLine = this.contentElement.createChild('div', 'status-dialog-line progress'); |
| this.progressLabel = progressLine.createChild('div', 'label'); |
| this.progressBar = progressLine.createChild('div', 'indicator-container').createChild('div', 'indicator'); |
| UI.ARIAUtils.markAsProgressBar(this.progressBar); |
| } |
| |
| if (typeof options.description === 'string') { |
| const descriptionLine = this.contentElement.createChild('div', 'status-dialog-line description'); |
| descriptionLine.createChild('div', 'label').textContent = i18nString(UIStrings.description); |
| this.description = descriptionLine.createChild('div', 'content'); |
| this.description.innerText = options.description; |
| } |
| |
| const buttonText = options.buttonText || i18nString(UIStrings.stop); |
| this.button = UI.UIUtils.createTextButton(buttonText, buttonCallback, '', true); |
| // Profiling can't be stopped during initialization. |
| this.button.disabled = !options.buttonDisabled === false; |
| this.contentElement.createChild('div', 'stop-button').appendChild(this.button); |
| } |
| |
| finish(): void { |
| this.stopTimer(); |
| this.button.disabled = true; |
| } |
| |
| remove(): void { |
| (this.element.parentNode as HTMLElement).classList.remove('tinted'); |
| this.arrangeDialog((this.element.parentNode as HTMLElement)); |
| this.stopTimer(); |
| this.element.remove(); |
| } |
| |
| showPane(parent: Element): void { |
| this.arrangeDialog(parent); |
| this.show(parent); |
| parent.classList.add('tinted'); |
| } |
| |
| enableAndFocusButton(): void { |
| this.button.disabled = false; |
| this.button.focus(); |
| } |
| |
| updateStatus(text: string): void { |
| this.status.textContent = text; |
| } |
| |
| updateProgressBar(activity: string, percent: number): void { |
| this.progressLabel.textContent = activity; |
| (this.progressBar as HTMLElement).style.width = percent.toFixed(1) + '%'; |
| UI.ARIAUtils.setValueNow(this.progressBar, percent); |
| this.updateTimer(); |
| } |
| |
| startTimer(): void { |
| this.startTime = Date.now(); |
| this.timeUpdateTimer = window.setInterval(this.updateTimer.bind(this, false), 1000); |
| this.updateTimer(); |
| } |
| |
| private stopTimer(): void { |
| if (!this.timeUpdateTimer) { |
| return; |
| } |
| clearInterval(this.timeUpdateTimer); |
| this.updateTimer(true); |
| delete this.timeUpdateTimer; |
| } |
| |
| private updateTimer(precise?: boolean): void { |
| this.arrangeDialog((this.element.parentNode as HTMLElement)); |
| if (!this.timeUpdateTimer) { |
| return; |
| } |
| const elapsed = (Date.now() - this.startTime) / 1000; |
| this.time.textContent = i18nString(UIStrings.ssec, {PH1: elapsed.toFixed(precise ? 1 : 0)}); |
| } |
| |
| private arrangeDialog(parent: Element): void { |
| const isSmallDialog = parent.clientWidth < 325; |
| this.element.classList.toggle('small-dialog', isSmallDialog); |
| this.contentElement.classList.toggle('small-dialog', isSmallDialog); |
| } |
| wasShown(): void { |
| super.wasShown(); |
| this.registerCSSFiles([timelineStatusDialogStyles]); |
| } |
| } |
| |
| let loadTimelineHandlerInstance: LoadTimelineHandler; |
| |
| export class LoadTimelineHandler implements Common.QueryParamHandler.QueryParamHandler { |
| static instance(opts: { |
| forceNew: boolean|null, |
| } = {forceNew: null}): LoadTimelineHandler { |
| const {forceNew} = opts; |
| if (!loadTimelineHandlerInstance || forceNew) { |
| loadTimelineHandlerInstance = new LoadTimelineHandler(); |
| } |
| |
| return loadTimelineHandlerInstance; |
| } |
| |
| handleQueryParam(value: string): void { |
| void UI.ViewManager.ViewManager.instance().showView('timeline').then(() => { |
| TimelinePanel.instance().loadFromURL(window.decodeURIComponent(value)); |
| }); |
| } |
| } |
| |
| let actionDelegateInstance: ActionDelegate; |
| |
| export class ActionDelegate implements UI.ActionRegistration.ActionDelegate { |
| static instance(opts: { |
| forceNew: boolean|null, |
| }|undefined = {forceNew: null}): ActionDelegate { |
| const {forceNew} = opts; |
| if (!actionDelegateInstance || forceNew) { |
| actionDelegateInstance = new ActionDelegate(); |
| } |
| |
| return actionDelegateInstance; |
| } |
| |
| handleAction(context: UI.Context.Context, actionId: string): boolean { |
| const panel = (UI.Context.Context.instance().flavor(TimelinePanel) as TimelinePanel); |
| console.assert(panel && panel instanceof TimelinePanel); |
| switch (actionId) { |
| case 'timeline.toggle-recording': |
| panel.toggleRecording(); |
| return true; |
| case 'timeline.record-reload': |
| panel.recordReload(); |
| return true; |
| case 'timeline.save-to-file': |
| void panel.saveToFile(); |
| return true; |
| case 'timeline.load-from-file': |
| panel.selectFileToLoad(); |
| return true; |
| case 'timeline.jump-to-previous-frame': |
| panel.jumpToFrame(-1); |
| return true; |
| case 'timeline.jump-to-next-frame': |
| panel.jumpToFrame(1); |
| return true; |
| case 'timeline.show-history': |
| void panel.showHistory(); |
| return true; |
| case 'timeline.previous-recording': |
| panel.navigateHistory(1); |
| return true; |
| case 'timeline.next-recording': |
| panel.navigateHistory(-1); |
| return true; |
| } |
| return false; |
| } |
| } |
| |
| const traceProviderToSetting = |
| new WeakMap<Extensions.ExtensionTraceProvider.ExtensionTraceProvider, Common.Settings.Setting<boolean>>(); |