UNPKG

chrome-devtools-frontend

Version:
1,456 lines (1,312 loc) 56.9 kB
/* * 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 Bindings from '../bindings/bindings.js'; import * as Common from '../common/common.js'; import * as Coverage from '../coverage/coverage.js'; // eslint-disable-line no-unused-vars import * as Extensions from '../extensions/extensions.js'; import * as Host from '../host/host.js'; import * as i18n from '../i18n/i18n.js'; import * as MobileThrottling from '../mobile_throttling/mobile_throttling.js'; import * as PerfUI from '../perf_ui/perf_ui.js'; import * as Platform from '../platform/platform.js'; import * as ProtocolClient from '../protocol_client/protocol_client.js'; import * as Root from '../root/root.js'; import * as SDK from '../sdk/sdk.js'; import * as TimelineModel from '../timeline_model/timeline_model.js'; import * as UI from '../ui/ui.js'; import {Events, PerformanceModel, Window} from './PerformanceModel.js'; // eslint-disable-line no-unused-vars import {Client, TimelineController} from './TimelineController.js'; // eslint-disable-line no-unused-vars import {TimelineEventOverview, TimelineEventOverviewCoverage, TimelineEventOverviewCPUActivity, TimelineEventOverviewFrames, TimelineEventOverviewInput, TimelineEventOverviewMemory, TimelineEventOverviewNetwork, TimelineEventOverviewResponsiveness, TimelineFilmStripOverview,} from './TimelineEventOverview.js'; // eslint-disable-line no-unused-vars 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'; export 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 *@example {reload} PH3 *@example {Ctrl + R} PH4 */ clickTheRecordButtonSOrHitSTo: 'Click the record button {PH1} or hit {PH2} to start a new recording. Click the reload button {PH3} or hit {PH4} 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('timeline/TimelinePanel.js', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); /** @type {!TimelinePanel} */ let timelinePanelInstance; /** * @implements {Client} * @implements {TimelineModeViewDelegate} */ export class TimelinePanel extends UI.Panel.Panel { constructor() { super('timeline'); this.registerRequiredCSS('timeline/timelinePanel.css', {enableLegacyPatching: true}); 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)); /** @type {!Array<!UI.Toolbar.ToolbarItem>} */ this._recordingOptionUIControls = []; this._state = State.Idle; this._recordingPageReload = false; this._millisecondsToRecordAfterLoadEvent = 5000; /** @type {!UI.ActionRegistration.Action }*/ this._toggleRecordAction = /** @type {!UI.ActionRegistration.Action }*/ ( UI.ActionRegistry.ActionRegistry.instance().action('timeline.toggle-recording')); /** @type {!UI.ActionRegistration.Action }*/ this._recordReloadAction = /** @type {!UI.ActionRegistration.Action }*/ ( UI.ActionRegistry.ActionRegistry.instance().action('timeline.record-reload')); this._historyManager = new TimelineHistoryManager(); /** @type {?PerformanceModel} */ 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._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); /** @type {!Array<!TimelineEventOverview>} */ this._overviewControls = []; this._statusPaneContainer = this._timelinePane.element.createChild('div', 'status-pane-container fill'); this._createFileSelector(); SDK.SDKModel.TargetManager.instance().addModelListener( SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.Load, this._loadEventFired, this); this._flameChart = new TimelineFlameChartView(this); this._searchableView = new UI.SearchableView.SearchableView(this._flameChart, null); this._searchableView.setMinimumSize(0, 100); this._searchableView.element.classList.add('searchable-view'); this._searchableView.show(this._timelinePane.element); this._flameChart.show(this._searchableView.element); this._flameChart.setSearchableView(this._searchableView); this._searchableView.hideWidget(); this._onModeChanged(); this._onWebVitalsChanged(); this._populateToolbar(); this._showLandingPage(); this._updateTimelineControls(); Extensions.ExtensionServer.ExtensionServer.instance().addEventListener( Extensions.ExtensionServer.Events.TraceProviderAdded, this._appendExtensionsToToolbar, this); SDK.SDKModel.TargetManager.instance().addEventListener( SDK.SDKModel.Events.SuspendStateChanged, this._onSuspendStateChanged, this); /** @type {!UI.Toolbar.ToolbarSettingToggle} */ this._showSettingsPaneButton; /** @type {!Common.Settings.Setting<boolean>} */ this._showSettingsPaneSetting; /** @type {!UI.Widget.Widget} */ this._settingsPane; /** @type {?TimelineController} */ this._controller; /** @type {!UI.Toolbar.ToolbarButton} */ this._clearButton; /** @type {!UI.Toolbar.ToolbarButton} */ this._loadButton; /** @type {!UI.Toolbar.ToolbarButton} */ this._saveButton; /** @type {?StatusPane} */ this._statusPane; /** @type {!UI.Widget.Widget} */ this._landingPage; } /** * @param {{forceNew: ?boolean}=} opts * @return {!TimelinePanel} */ static instance(opts = {forceNew: null}) { const {forceNew} = opts; if (!timelinePanelInstance || forceNew) { timelinePanelInstance = new TimelinePanel(); } return timelinePanelInstance; } /** * @override * @return {?UI.SearchableView.SearchableView} */ searchableView() { return this._searchableView; } /** * @override */ wasShown() { UI.Context.Context.instance().setFlavor(TimelinePanel, this); // Record the performance tool load time. Host.userMetrics.panelLoaded('timeline', 'DevTools.Launch.Timeline'); } /** * @override */ willHide() { UI.Context.Context.instance().setFlavor(TimelinePanel, null); this._historyManager.cancelIfShowing(); } /** * @param {!Array.<!SDK.TracingManager.EventPayload>} events */ loadFromEvents(events) { if (this._state !== State.Idle) { return; } this._prepareToLoadTimeline(); this._loader = TimelineLoader.loadFromEvents(events, this); } /** * @param {!Common.EventTarget.EventTargetEvent} event */ _onOverviewWindowChanged(event) { if (!this._performanceModel) { return; } const left = event.data.startTime; const right = event.data.endTime; this._performanceModel.setWindow({left, right}, /* animate */ true); } /** * @param {!Common.EventTarget.EventTargetEvent} event */ _onModelWindowChanged(event) { const window = /** @type {!Window} */ (event.data.window); this._overviewPane.setWindowTimes(window.left, window.right); } /** * @param {!State} state */ _setState(state) { this._state = state; this._updateTimelineControls(); } /** * @param {!Common.Settings.Setting<?>} setting * @param {string} tooltip * @return {!UI.Toolbar.ToolbarItem} */ _createSettingCheckbox(setting, tooltip) { const checkboxItem = new UI.Toolbar.ToolbarSettingCheckbox(setting, tooltip); this._recordingOptionUIControls.push(checkboxItem); return checkboxItem; } _populateToolbar() { // 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, () => this._selectFileToLoad()); this._saveButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.saveProfile), 'largeicon-download'); this._saveButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, event => { 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.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); } _createSettingsPane() { 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); MobileThrottling.ThrottlingManager.throttlingManager().addEventListener( MobileThrottling.ThrottlingManager.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(); } /** * @param {!Common.EventTarget.EventTargetEvent} event */ _appendExtensionsToToolbar(event) { const provider = /** @type {!Extensions.ExtensionTraceProvider.ExtensionTraceProvider} */ (event.data); const setting = TimelinePanel._settingForTraceProvider(provider); const checkbox = this._createSettingCheckbox(setting, provider.longDisplayName()); this._panelToolbar.appendToolbarItem(checkbox); } /** * @param {!Extensions.ExtensionTraceProvider.ExtensionTraceProvider} traceProvider * @return {!Common.Settings.Setting<boolean>} */ static _settingForTraceProvider(traceProvider) { 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; } /** * @return {!UI.Toolbar.ToolbarComboBox} */ _createNetworkConditionsSelect() { const toolbarItem = new UI.Toolbar.ToolbarComboBox(null, i18nString(UIStrings.networkConditions)); toolbarItem.setMaxWidth(140); MobileThrottling.ThrottlingManager.throttlingManager().decorateSelectWithNetworkThrottling( toolbarItem.selectElement()); return toolbarItem; } _prepareToLoadTimeline() { console.assert(this._state === State.Idle); this._setState(State.Loading); if (this._performanceModel) { this._performanceModel.dispose(); this._performanceModel = null; } } _createFileSelector() { if (this._fileSelectorElement) { this._fileSelectorElement.remove(); } this._fileSelectorElement = UI.UIUtils.createFileSelectorElement(this._loadFromFile.bind(this)); this._timelinePane.element.appendChild(this._fileSelectorElement); } /** * @param {!Event} event */ _contextMenu(event) { const contextMenu = new UI.ContextMenu.ContextMenu(event); contextMenu.appendItemsAtLocation('timelineMenu'); contextMenu.show(); } async _saveToFile() { 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'; const stream = new Bindings.FileUtils.FileOutputStream(); const accepted = await stream.open(fileName); if (!accepted) { return; } const error = /** @type {?{message: string, name: string, code: number}} */ (await performanceModel.save(stream)); if (!error) { return; } Common.Console.Console.instance().error( i18nString(UIStrings.failedToSaveTimelineSSS, {PH1: error.message, PH2: error.name, PH3: error.code})); } async _showHistory() { const model = await this._historyManager.showHistoryDropDown(); if (model && model !== this._performanceModel) { this._setModel(model); } } /** * @param {number} direction * @return {boolean} */ _navigateHistory(direction) { const model = this._historyManager.navigate(direction); if (model && model !== this._performanceModel) { this._setModel(model); } return true; } _selectFileToLoad() { if (this._fileSelectorElement) { this._fileSelectorElement.click(); } } /** * @param {!File} file */ _loadFromFile(file) { if (this._state !== State.Idle) { return; } this._prepareToLoadTimeline(); this._loader = TimelineLoader.loadFromFile(file, this); this._createFileSelector(); } /** * @param {string} url */ _loadFromURL(url) { if (this._state !== State.Idle) { return; } this._prepareToLoadTimeline(); this._loader = TimelineLoader.loadFromURL(url, this); } _updateOverviewControls() { 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); } _onModeChanged() { this._updateOverviewControls(); this.doResize(); this.select(null); } _onWebVitalsChanged() { this._flameChart.toggleWebVitalsLane(); } _updateSettingsPaneVisibility() { if (this._showSettingsPaneSetting.get()) { this._settingsPane.showWidget(); } else { this._settingsPane.hideWidget(); } } _updateShowSettingsToolbarButton() { /** @type {!Array<string>} */ const messages = []; if (MobileThrottling.ThrottlingManager.throttlingManager().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)); } } /** * @param {boolean} enabled */ _setUIControlsEnabled(enabled) { this._recordingOptionUIControls.forEach(control => control.setEnabled(enabled)); } async _getCoverageViewWidget() { const view = /** @type {!UI.View.View} */ (UI.ViewManager.ViewManager.instance().view('coverage')); return /** @type {!Coverage.CoverageView.CoverageView} */ (await view.widget()); } async _startRecording() { 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 = /** @type {!SDK.SDKModel.Target} */ (SDK.SDKModel.TargetManager.instance().mainTarget()); if (UIDevtoolsUtils.isUiDevTools()) { this._controller = new UIDevtoolsController(mainTarget, this); } else { this._controller = new TimelineController(mainTarget, this); } this._setUIControlsEnabled(false); this._hideLandingPage(); const response = await this._controller.startRecording(recordingOptions, enabledTraceProviders); // @ts-ignore crbug.com/1011811 Closure does not understand `.getError()` propagation from the tracing model if (response[ProtocolClient.InspectorBackend.ProtocolError]) { // @ts-ignore crbug.com/1011811 Closure does not understand `.getError()` propagation from the tracing model this._recordingFailed(response[ProtocolClient.InspectorBackend.ProtocolError]); } else { this._recordingStarted(); } } async _stopRecording() { 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; } } /** * @param {string} error The error message to display */ _recordingFailed(error) { if (this._statusPane) { this._statusPane.hide(); } 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; } } _onSuspendStateChanged() { this._updateTimelineControls(); } _updateTimelineControls() { 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() { if (this._state === State.Idle) { this._recordingPageReload = false; this._startRecording(); Host.userMetrics.actionTaken(Host.UserMetrics.Action.TimelineStarted); } else if (this._state === State.Recording) { this._stopRecording(); } } _recordReload() { if (this._state !== State.Idle) { return; } this._recordingPageReload = true; this._startRecording(); Host.userMetrics.actionTaken(Host.UserMetrics.Action.TimelinePageReloadStarted); } _onClearButton() { this._historyManager.clear(); this._clear(); } _clear() { this._showLandingPage(); this._reset(); } _reset() { PerfUI.LineLevelProfile.Performance.instance().reset(); this._setModel(null); } /** * @param {!PerformanceModel} model */ _applyFilters(model) { if (model.timelineModel().isGenericTrace() || Root.Runtime.experiments.isEnabled('timelineShowAllEvents')) { return; } model.setFilters([TimelineUIUtils.visibleEventsFilter()]); } /** * @param {?PerformanceModel} model */ _setModel(model) { if (this._performanceModel) { this._performanceModel.removeEventListener(Events.WindowChanged, this._onModelWindowChanged, this); } this._performanceModel = model; if (model) { this._searchableView.showWidget(); this._applyFilters(model); } else { this._searchableView.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(); } _recordingStarted() { 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(); } /** * @override * @param {number} usage */ recordingProgress(usage) { if (this._statusPane) { this._statusPane.updateProgressBar(i18nString(UIStrings.bufferUsage), usage * 100); } } _showLandingPage() { if (this._landingPage) { this._landingPage.show(this._statusPaneContainer); return; } /** * @param {string} tagName * @param {string} contents */ function encloseWithTag(tagName, contents) { const e = document.createElement(tagName); e.textContent = contents; return e; } const learnMoreNode = UI.XLink.XLink.create( 'https://developers.google.com/web/tools/chrome-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, PH3: reloadButton, PH4: reloadKey})); centered.createChild('p').appendChild(i18n.i18n.getFormatLocalizedString( str_, UIStrings.afterRecordingSelectAnAreaOf, {PH1: navigateNode, PH2: learnMoreNode})); this._landingPage.show(this._statusPaneContainer); } _hideLandingPage() { this._landingPage.detach(); } /** * @override */ loadingStarted() { this._hideLandingPage(); if (this._statusPane) { this._statusPane.hide(); } 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); } /** * @override * @param {number=} progress */ loadingProgress(progress) { if (typeof progress === 'number' && this._statusPane) { this._statusPane.updateProgressBar(i18nString(UIStrings.received), progress * 100); } } /** * @override */ processingStarted() { if (this._statusPane) { this._statusPane.updateStatus(i18nString(UIStrings.processingProfile)); } } /** * @override * @param {?SDK.TracingModel.TracingModel} tracingModel */ loadingComplete(tracingModel) { delete this._loader; this._setState(State.Idle); if (this._statusPane) { this._statusPane.hide(); } 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()) { UI.ViewManager.ViewManager.instance() .showView('coverage') .then(() => this._getCoverageViewWidget()) .then(widget => widget.processBacklog()) .then(() => this._updateOverviewControls()); } } _showRecordingStarted() { 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)); } _cancelLoading() { if (this._loader) { this._loader.cancel(); } } /** * @param {!TimelineModel.TimelineModel.TimelineModelImpl} timelineModel */ _setMarkers(timelineModel) { const markers = new Map(); 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); } /** * @param {!Common.EventTarget.EventTargetEvent} event */ async _loadEventFired(event) { 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 => setTimeout(r, this._millisecondsToRecordAfterLoadEvent)); // Check if we're still in the same recording session. if (controller !== this._controller || this._state !== State.Recording) { return; } this._stopRecording(); } /** * @param {!TimelineSelection} selection * @return {?TimelineModel.TimelineFrameModel.TimelineFrame} */ _frameForSelection(selection) { switch (selection.type()) { case TimelineSelection.Type.Frame: return /** @type {!TimelineModel.TimelineFrameModel.TimelineFrame} */ (selection.object()); case TimelineSelection.Type.Range: return null; case TimelineSelection.Type.TraceEvent: if (!this._performanceModel) { return null; } return this._performanceModel.frameModel().frames(selection._endTime, selection._endTime)[0]; default: console.assert(false, 'Should never be reached'); return null; } } /** * @param {number} offset */ _jumpToFrame(offset) { 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; } /** * @override * @param {?TimelineSelection} selection */ select(selection) { this._selection = selection; this._flameChart.setSelection(selection); } /** * @override * @param {?Array<!SDK.TracingModel.Event>} events * @param {number} time */ selectEntryAtTime(events, time) { 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); } /** * @override * @param {?SDK.TracingModel.Event} event */ highlightEvent(event) { this._flameChart.highlightEvent(event); } /** * @param {number} startTime * @param {number} endTime */ _revealTimeRange(startTime, endTime) { 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); } /** * @param {!DataTransfer} dataTransfer */ _handleDrop(dataTransfer) { const items = dataTransfer.items; if (!items.length) { return; } const item = items[0]; 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 entry = items[0].webkitGetAsEntry(); if (!entry.isFile) { return; } entry.file(this._loadFromFile.bind(this)); } } } /** * @enum {symbol} */ export const State = { Idle: Symbol('Idle'), StartPending: Symbol('StartPending'), Recording: Symbol('Recording'), StopPending: Symbol('StopPending'), Loading: Symbol('Loading'), RecordingFailed: Symbol('RecordingFailed') }; /** * @enum {string} */ export const 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 { /** * @param {!TimelineSelection.Type} type * @param {number} startTime * @param {number} endTime * @param {!Object=} object */ constructor(type, startTime, endTime, object) { this._type = type; this._startTime = startTime; this._endTime = endTime; this._object = object || null; } /** * @param {!TimelineModel.TimelineFrameModel.TimelineFrame} frame * @return {!TimelineSelection} */ static fromFrame(frame) { return new TimelineSelection(TimelineSelection.Type.Frame, frame.startTime, frame.endTime, frame); } /** * @param {!TimelineModel.TimelineModel.NetworkRequest} request * @return {!TimelineSelection} */ static fromNetworkRequest(request) { return new TimelineSelection( TimelineSelection.Type.NetworkRequest, request.startTime, request.endTime || request.startTime, request); } /** * @param {!SDK.TracingModel.Event} event * @return {!TimelineSelection} */ static fromTraceEvent(event) { return new TimelineSelection( TimelineSelection.Type.TraceEvent, event.startTime, event.endTime || (event.startTime + 1), event); } /** * @param {number} startTime * @param {number} endTime * @return {!TimelineSelection} */ static fromRange(startTime, endTime) { return new TimelineSelection(TimelineSelection.Type.Range, startTime, endTime); } /** * @return {!TimelineSelection.Type} */ type() { return this._type; } /** * @return {?Object} */ object() { return this._object; } /** * @return {number} */ startTime() { return this._startTime; } /** * @return {number} */ endTime() { return this._endTime; } } /** * @enum {string} */ TimelineSelection.Type = { Frame: 'Frame', NetworkRequest: 'NetworkRequest', TraceEvent: 'TraceEvent', Range: 'Range' }; /** * @interface */ export class TimelineModeViewDelegate { /** * @param {?TimelineSelection} selection */ select(selection) { } /** * @param {?Array<!SDK.TracingModel.Event>} events * @param {number} time */ selectEntryAtTime(events, time) { } /** * @param {?SDK.TracingModel.Event} event */ highlightEvent(event) { } } export class StatusPane extends UI.Widget.VBox { /** * @param {!{showTimer: (boolean|undefined), showProgress: (boolean|undefined), description: (string|undefined), butto