| /* |
| * 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. |
| */ |
| |
| /** |
| * @implements {Timeline.TimelineController.Client} |
| * @implements {Timeline.TimelineModeViewDelegate} |
| * @unrestricted |
| */ |
| Timeline.TimelinePanel = class extends UI.Panel { |
| constructor() { |
| super('timeline'); |
| this.registerRequiredCSS('timeline/timelinePanel.css'); |
| this.element.addEventListener('contextmenu', this._contextMenu.bind(this), false); |
| this._dropTarget = new UI.DropTarget( |
| this.element, [UI.DropTarget.Type.File, UI.DropTarget.Type.URI], |
| Common.UIString('Drop timeline file or URL here'), this._handleDrop.bind(this)); |
| |
| /** @type {!Array<!UI.ToolbarItem>} */ |
| this._recordingOptionUIControls = []; |
| this._state = Timeline.TimelinePanel.State.Idle; |
| this._recordingPageReload = false; |
| this._millisecondsToRecordAfterLoadEvent = 3000; |
| this._toggleRecordAction = |
| /** @type {!UI.Action }*/ (UI.actionRegistry.action('timeline.toggle-recording')); |
| this._recordReloadAction = |
| /** @type {!UI.Action }*/ (UI.actionRegistry.action('timeline.record-reload')); |
| |
| this._historyManager = new Timeline.TimelineHistoryManager(); |
| |
| /** @type {?Timeline.PerformanceModel} */ |
| this._performanceModel = null; |
| |
| this._viewModeSetting = |
| Common.settings.createSetting('timelineViewMode', Timeline.TimelinePanel.ViewMode.FlameChart); |
| |
| this._disableCaptureJSProfileSetting = Common.settings.createSetting('timelineDisableJSSampling', false); |
| this._disableCaptureJSProfileSetting.setTitle(Common.UIString('Disable JavaScript samples')); |
| this._captureLayersAndPicturesSetting = Common.settings.createSetting('timelineCaptureLayersAndPictures', false); |
| this._captureLayersAndPicturesSetting.setTitle(Common.UIString('Enable advanced paint instrumentation (slow)')); |
| |
| this._showScreenshotsSetting = Common.settings.createSetting('timelineShowScreenshots', false); |
| this._showScreenshotsSetting.setTitle(Common.UIString('Screenshots')); |
| this._showScreenshotsSetting.addChangeListener(this._updateOverviewControls, this); |
| |
| this._startCoverage = Common.settings.createSetting('timelineStartCoverage', false); |
| this._startCoverage.setTitle(ls`Coverage`); |
| |
| if (!Root.Runtime.experiments.isEnabled('recordCoverageWithPerformanceTracing')) { |
| this._startCoverage.set(false); |
| } |
| |
| |
| this._showMemorySetting = Common.settings.createSetting('timelineShowMemory', false); |
| this._showMemorySetting.setTitle(Common.UIString('Memory')); |
| this._showMemorySetting.addChangeListener(this._onModeChanged, this); |
| |
| const timelineToolbarContainer = this.element.createChild('div', 'timeline-toolbar-container'); |
| this._panelToolbar = new UI.Toolbar('timeline-main-toolbar', timelineToolbarContainer); |
| this._panelRightToolbar = new UI.Toolbar('', timelineToolbarContainer); |
| this._createSettingsPane(); |
| this._updateShowSettingsToolbarButton(); |
| |
| this._timelinePane = new UI.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('timeline'); |
| this._overviewPane.addEventListener( |
| PerfUI.TimelineOverviewPane.Events.WindowChanged, this._onOverviewWindowChanged.bind(this)); |
| this._overviewPane.show(topPaneElement); |
| /** @type {!Array<!Timeline.TimelineEventOverview>} */ |
| this._overviewControls = []; |
| |
| this._statusPaneContainer = this._timelinePane.element.createChild('div', 'status-pane-container fill'); |
| |
| this._createFileSelector(); |
| |
| SDK.targetManager.addModelListener( |
| SDK.ResourceTreeModel, SDK.ResourceTreeModel.Events.Load, this._loadEventFired, this); |
| |
| this._flameChart = new Timeline.TimelineFlameChartView(this); |
| this._searchableView = new UI.SearchableView(this._flameChart); |
| 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._onModeChanged(); |
| this._populateToolbar(); |
| this._showLandingPage(); |
| this._updateTimelineControls(); |
| |
| Extensions.extensionServer.addEventListener( |
| Extensions.ExtensionServer.Events.TraceProviderAdded, this._appendExtensionsToToolbar, this); |
| SDK.targetManager.addEventListener(SDK.TargetManager.Events.SuspendStateChanged, this._onSuspendStateChanged, this); |
| } |
| |
| /** |
| * @return {!Timeline.TimelinePanel} |
| */ |
| static instance() { |
| return /** @type {!Timeline.TimelinePanel} */ (self.runtime.sharedInstance(Timeline.TimelinePanel)); |
| } |
| |
| /** |
| * @override |
| * @return {?UI.SearchableView} |
| */ |
| searchableView() { |
| return this._searchableView; |
| } |
| |
| /** |
| * @override |
| */ |
| wasShown() { |
| UI.context.setFlavor(Timeline.TimelinePanel, this); |
| } |
| |
| /** |
| * @override |
| */ |
| willHide() { |
| UI.context.setFlavor(Timeline.TimelinePanel, null); |
| this._historyManager.cancelIfShowing(); |
| } |
| |
| /** |
| * @param {!Array.<!SDK.TracingManager.EventPayload>} events |
| */ |
| loadFromEvents(events) { |
| if (this._state !== Timeline.TimelinePanel.State.Idle) { |
| return; |
| } |
| this._prepareToLoadTimeline(); |
| this._loader = Timeline.TimelineLoader.loadFromEvents(events, this); |
| } |
| |
| /** |
| * @param {!Common.Event} event |
| */ |
| _onOverviewWindowChanged(event) { |
| const left = event.data.startTime; |
| const right = event.data.endTime; |
| this._performanceModel.setWindow({left, right}, /* animate */ true); |
| } |
| |
| /** |
| * @param {!Common.Event} event |
| */ |
| _onModelWindowChanged(event) { |
| const window = /** @type {!Timeline.PerformanceModel.Window} */ (event.data.window); |
| this._overviewPane.setWindowTimes(window.left, window.right); |
| } |
| |
| /** |
| * @param {!Timeline.TimelinePanel.State} state |
| */ |
| _setState(state) { |
| this._state = state; |
| this._updateTimelineControls(); |
| } |
| |
| /** |
| * @param {!Common.Setting} setting |
| * @param {string} tooltip |
| * @return {!UI.ToolbarItem} |
| */ |
| _createSettingCheckbox(setting, tooltip) { |
| const checkboxItem = new UI.ToolbarSettingCheckbox(setting, tooltip); |
| this._recordingOptionUIControls.push(checkboxItem); |
| return checkboxItem; |
| } |
| |
| _populateToolbar() { |
| // Record |
| this._panelToolbar.appendToolbarItem(UI.Toolbar.createActionButton(this._toggleRecordAction)); |
| this._panelToolbar.appendToolbarItem(UI.Toolbar.createActionButton(this._recordReloadAction)); |
| this._clearButton = new UI.ToolbarButton(Common.UIString('Clear'), 'largeicon-clear'); |
| this._clearButton.addEventListener(UI.ToolbarButton.Events.Click, () => this._onClearButton()); |
| this._panelToolbar.appendToolbarItem(this._clearButton); |
| |
| // Load / Save |
| this._loadButton = new UI.ToolbarButton(Common.UIString('Load profile...'), 'largeicon-load'); |
| this._loadButton.addEventListener(UI.ToolbarButton.Events.Click, () => this._selectFileToLoad()); |
| this._saveButton = new UI.ToolbarButton(Common.UIString('Save profile...'), 'largeicon-download'); |
| this._saveButton.addEventListener(UI.ToolbarButton.Events.Click, () => 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, Common.UIString('Capture screenshots')); |
| // this._panelToolbar.appendToolbarItem(this._showScreenshotsToolbarCheckbox); |
| |
| // this._showMemoryToolbarCheckbox = |
| // this._createSettingCheckbox(this._showMemorySetting, Common.UIString('Show memory timeline')); |
| // this._panelToolbar.appendToolbarItem(this._showMemoryToolbarCheckbox); |
| |
| if (Root.Runtime.experiments.isEnabled('recordCoverageWithPerformanceTracing')) { |
| this._startCoverageCheckbox = |
| this._createSettingCheckbox(this._startCoverage, ls`Record coverage with performance trace`); |
| this._panelToolbar.appendToolbarItem(this._startCoverageCheckbox); |
| } |
| |
| // GC |
| // this._panelToolbar.appendToolbarItem(UI.Toolbar.createActionButtonForId('components.collect-garbage')); |
| |
| // Settings |
| // this._panelRightToolbar.appendSeparator(); |
| // this._panelRightToolbar.appendToolbarItem(this._showSettingsPaneButton); |
| } |
| |
| _createSettingsPane() { |
| this._showSettingsPaneSetting = Common.settings.createSetting('timelineShowSettingsToolbar', false); |
| this._showSettingsPaneButton = new UI.ToolbarSettingToggle( |
| this._showSettingsPaneSetting, 'largeicon-settings-gear', Common.UIString('Capture settings')); |
| SDK.multitargetNetworkManager.addEventListener( |
| SDK.MultitargetNetworkManager.Events.ConditionsChanged, this._updateShowSettingsToolbarButton, this); |
| MobileThrottling.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.HBox(); |
| this._settingsPane.element.classList.add('timeline-settings-pane'); |
| this._settingsPane.show(this.element); |
| |
| const captureToolbar = new UI.Toolbar('', this._settingsPane.element); |
| captureToolbar.element.classList.add('flex-auto'); |
| captureToolbar.makeVertical(); |
| captureToolbar.appendToolbarItem(this._createSettingCheckbox( |
| this._disableCaptureJSProfileSetting, |
| Common.UIString('Disables JavaScript sampling, reduces overhead when running against mobile devices'))); |
| captureToolbar.appendToolbarItem(this._createSettingCheckbox( |
| this._captureLayersAndPicturesSetting, |
| Common.UIString('Captures advanced paint instrumentation, introduces significant performance overhead'))); |
| |
| const throttlingPane = new UI.VBox(); |
| throttlingPane.element.classList.add('flex-auto'); |
| throttlingPane.show(this._settingsPane.element); |
| |
| const networkThrottlingToolbar = new UI.Toolbar('', throttlingPane.element); |
| networkThrottlingToolbar.appendText(Common.UIString('Network:')); |
| this._networkThrottlingSelect = this._createNetworkConditionsSelect(); |
| networkThrottlingToolbar.appendToolbarItem(this._networkThrottlingSelect); |
| |
| const cpuThrottlingToolbar = new UI.Toolbar('', throttlingPane.element); |
| cpuThrottlingToolbar.appendText(Common.UIString('CPU:')); |
| this._cpuThrottlingSelect = MobileThrottling.throttlingManager().createCPUThrottlingSelector(); |
| cpuThrottlingToolbar.appendToolbarItem(this._cpuThrottlingSelect); |
| |
| this._showSettingsPaneSetting.addChangeListener(this._updateSettingsPaneVisibility.bind(this)); |
| this._updateSettingsPaneVisibility(); |
| } |
| |
| /** |
| * @param {!Common.Event} event |
| */ |
| _appendExtensionsToToolbar(event) { |
| const provider = /** @type {!Extensions.ExtensionTraceProvider} */ (event.data); |
| const setting = Timeline.TimelinePanel._settingForTraceProvider(provider); |
| const checkbox = this._createSettingCheckbox(setting, provider.longDisplayName()); |
| this._panelToolbar.appendToolbarItem(checkbox); |
| } |
| |
| /** |
| * @param {!Extensions.ExtensionTraceProvider} traceProvider |
| * @return {!Common.Setting<boolean>} |
| */ |
| static _settingForTraceProvider(traceProvider) { |
| let setting = traceProvider[Timeline.TimelinePanel._traceProviderSettingSymbol]; |
| if (!setting) { |
| const providerId = traceProvider.persistentIdentifier(); |
| setting = Common.settings.createSetting(providerId, false); |
| setting.setTitle(traceProvider.shortDisplayName()); |
| traceProvider[Timeline.TimelinePanel._traceProviderSettingSymbol] = setting; |
| } |
| return setting; |
| } |
| |
| /** |
| * @return {!UI.ToolbarComboBox} |
| */ |
| _createNetworkConditionsSelect() { |
| const toolbarItem = new UI.ToolbarComboBox(null, ls`Network conditions`); |
| toolbarItem.setMaxWidth(140); |
| MobileThrottling.throttlingManager().decorateSelectWithNetworkThrottling(toolbarItem.selectElement()); |
| return toolbarItem; |
| } |
| |
| _prepareToLoadTimeline() { |
| console.assert(this._state === Timeline.TimelinePanel.State.Idle); |
| this._setState(Timeline.TimelinePanel.State.Loading); |
| if (this._performanceModel) { |
| this._performanceModel.dispose(); |
| this._performanceModel = null; |
| } |
| } |
| |
| _createFileSelector() { |
| if (this._fileSelectorElement) { |
| this._fileSelectorElement.remove(); |
| } |
| this._fileSelectorElement = UI.createFileSelectorElement(this._loadFromFile.bind(this)); |
| this._timelinePane.element.appendChild(this._fileSelectorElement); |
| } |
| |
| /** |
| * @param {!Event} event |
| */ |
| _contextMenu(event) { |
| const contextMenu = new UI.ContextMenu(event); |
| contextMenu.appendItemsAtLocation('timelineMenu'); |
| contextMenu.show(); |
| } |
| |
| /** |
| * @suppress {deprecated} |
| */ |
| async _saveToFile() { |
| if (this._state !== Timeline.TimelinePanel.State.Idle) { |
| return; |
| } |
| const performanceModel = this._performanceModel; |
| if (!performanceModel) { |
| return; |
| } |
| |
| const now = new Date(); |
| const fileName = 'Profile-' + now.toISO8601Compact() + '.json'; |
| const stream = new Bindings.FileOutputStream(); |
| |
| const accepted = await stream.open(fileName); |
| if (!accepted) { |
| return; |
| } |
| |
| const error = await performanceModel.save(stream); |
| if (!error) { |
| return; |
| } |
| Common.console.error( |
| Common.UIString('Failed to save timeline: %s (%s, %s)', error.message, error.name, 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() { |
| this._fileSelectorElement.click(); |
| } |
| |
| /** |
| * @param {!File} file |
| */ |
| _loadFromFile(file) { |
| if (this._state !== Timeline.TimelinePanel.State.Idle) { |
| return; |
| } |
| this._prepareToLoadTimeline(); |
| this._loader = Timeline.TimelineLoader.loadFromFile(file, this); |
| this._createFileSelector(); |
| } |
| |
| /** |
| * @param {string} url |
| */ |
| _loadFromURL(url) { |
| if (this._state !== Timeline.TimelinePanel.State.Idle) { |
| return; |
| } |
| this._prepareToLoadTimeline(); |
| this._loader = Timeline.TimelineLoader.loadFromURL(url, this); |
| } |
| |
| _updateOverviewControls() { |
| this._overviewControls = []; |
| this._overviewControls.push(new Timeline.TimelineEventOverviewResponsiveness()); |
| if (Root.Runtime.experiments.isEnabled('inputEventsOnTimelineOverview')) { |
| this._overviewControls.push(new Timeline.TimelineEventOverviewInput()); |
| } |
| this._overviewControls.push(new Timeline.TimelineEventOverviewFrames()); |
| this._overviewControls.push(new Timeline.TimelineEventOverviewCPUActivity()); |
| this._overviewControls.push(new Timeline.TimelineEventOverviewNetwork()); |
| if (this._showScreenshotsSetting.get() && this._performanceModel && |
| this._performanceModel.filmStripModel().frames().length) { |
| this._overviewControls.push(new Timeline.TimelineFilmStripOverview()); |
| } |
| if (this._showMemorySetting.get()) { |
| this._overviewControls.push(new Timeline.TimelineEventOverviewMemory()); |
| } |
| if (this._startCoverage.get()) { |
| this._overviewControls.push(new Timeline.TimelineEventOverviewCoverage()); |
| } |
| for (const control of this._overviewControls) { |
| control.setModel(this._performanceModel); |
| } |
| this._overviewPane.setOverviewControls(this._overviewControls); |
| } |
| |
| _onModeChanged() { |
| this._updateOverviewControls(); |
| this.doResize(); |
| this.select(null); |
| } |
| |
| _updateSettingsPaneVisibility() { |
| if (this._showSettingsPaneSetting.get()) { |
| this._settingsPane.showWidget(); |
| } else { |
| this._settingsPane.hideWidget(); |
| } |
| } |
| |
| _updateShowSettingsToolbarButton() { |
| const messages = []; |
| if (MobileThrottling.throttlingManager().cpuThrottlingRate() !== 1) { |
| messages.push(Common.UIString('- CPU throttling is enabled')); |
| } |
| if (SDK.multitargetNetworkManager.isThrottling()) { |
| messages.push(Common.UIString('- Network throttling is enabled')); |
| } |
| if (this._captureLayersAndPicturesSetting.get()) { |
| messages.push(Common.UIString('- Significant overhead due to paint instrumentation')); |
| } |
| if (this._disableCaptureJSProfileSetting.get()) { |
| messages.push(Common.UIString('- JavaScript sampling is disabled')); |
| } |
| |
| this._showSettingsPaneButton.setDefaultWithRedColor(messages.length); |
| this._showSettingsPaneButton.setToggleWithRedColor(messages.length); |
| |
| if (messages.length) { |
| const tooltipElement = createElement('div'); |
| messages.forEach(message => { |
| tooltipElement.createChild('div').textContent = message; |
| }); |
| this._showSettingsPaneButton.setTitle(tooltipElement); |
| } else { |
| this._showSettingsPaneButton.setTitle(Common.UIString('Capture settings')); |
| } |
| } |
| |
| /** |
| * @param {boolean} enabled |
| */ |
| _setUIControlsEnabled(enabled) { |
| this._recordingOptionUIControls.forEach(control => control.setEnabled(enabled)); |
| } |
| |
| async _startRecording() { |
| console.assert(!this._statusPane, 'Status pane is already opened.'); |
| this._setState(Timeline.TimelinePanel.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.showView('coverage') |
| .then(() => UI.viewManager.view('coverage').widget()) |
| .then(widget => widget.ensureRecordingStarted()); |
| } |
| |
| this._showRecordingStarted(); |
| |
| const enabledTraceProviders = Extensions.extensionServer.traceProviders().filter( |
| provider => Timeline.TimelinePanel._settingForTraceProvider(provider).get()); |
| |
| const mainTarget = /** @type {!SDK.Target} */ (SDK.targetManager.mainTarget()); |
| if (Timeline.UIDevtoolsUtils.isUiDevTools()) { |
| this._controller = new Timeline.UIDevtoolsController(mainTarget, this); |
| } else { |
| this._controller = new Timeline.TimelineController(mainTarget, this); |
| } |
| this._setUIControlsEnabled(false); |
| this._hideLandingPage(); |
| const response = await this._controller.startRecording(recordingOptions, enabledTraceProviders); |
| if (response[Protocol.Error]) { |
| this._recordingFailed(response[Protocol.Error]); |
| } else { |
| this._recordingStarted(); |
| } |
| } |
| |
| async _stopRecording() { |
| if (this._statusPane) { |
| this._statusPane.finish(); |
| this._statusPane.updateStatus(Common.UIString('Stopping timeline\u2026')); |
| this._statusPane.updateProgressBar(Common.UIString('Received'), 0); |
| } |
| this._setState(Timeline.TimelinePanel.State.StopPending); |
| 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 Timeline.TimelinePanel.StatusPane({description: error}, () => this.loadingComplete(null)); |
| this._statusPane.showPane(this._statusPaneContainer); |
| this._statusPane.updateStatus(ls`Recording failed`); |
| this._statusPane.updateButton(ls`Close`); |
| |
| this._setState(Timeline.TimelinePanel.State.RecordingFailed); |
| this._performanceModel = null; |
| this._setUIControlsEnabled(false); |
| this._controller.dispose(); |
| this._controller = null; |
| } |
| |
| _onSuspendStateChanged() { |
| this._updateTimelineControls(); |
| } |
| |
| _updateTimelineControls() { |
| const state = Timeline.TimelinePanel.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 && !!this._performanceModel); |
| } |
| |
| _toggleRecording() { |
| if (this._state === Timeline.TimelinePanel.State.Idle) { |
| this._recordingPageReload = false; |
| this._startRecording(); |
| Host.userMetrics.actionTaken(Host.UserMetrics.Action.TimelineStarted); |
| } else if (this._state === Timeline.TimelinePanel.State.Recording) { |
| this._stopRecording(); |
| } |
| } |
| |
| _recordReload() { |
| if (this._state !== Timeline.TimelinePanel.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() { |
| self.runtime.sharedInstance(PerfUI.LineLevelProfile.Performance).reset(); |
| this._setModel(null); |
| } |
| |
| /** |
| * @param {!Timeline.PerformanceModel} model |
| */ |
| _applyFilters(model) { |
| if (model.timelineModel().isGenericTrace() || Root.Runtime.experiments.isEnabled('timelineShowAllEvents')) { |
| return; |
| } |
| model.setFilters([Timeline.TimelineUIUtils.visibleEventsFilter()]); |
| } |
| |
| /** |
| * @param {?Timeline.PerformanceModel} model |
| */ |
| _setModel(model) { |
| if (this._performanceModel) { |
| this._performanceModel.removeEventListener( |
| Timeline.PerformanceModel.Events.WindowChanged, this._onModelWindowChanged, this); |
| } |
| this._performanceModel = model; |
| if (model) { |
| this._applyFilters(model); |
| } |
| this._flameChart.setModel(model); |
| |
| this._updateOverviewControls(); |
| this._overviewPane.reset(); |
| if (model) { |
| this._performanceModel.addEventListener( |
| Timeline.PerformanceModel.Events.WindowChanged, this._onModelWindowChanged, this); |
| this._overviewPane.setBounds( |
| model.timelineModel().minimumRecordTime(), model.timelineModel().maximumRecordTime()); |
| const lineLevelProfile = self.runtime.sharedInstance(PerfUI.LineLevelProfile.Performance); |
| lineLevelProfile.reset(); |
| for (const profile of model.timelineModel().cpuProfiles()) { |
| lineLevelProfile.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) { |
| const target = this._controller.mainTarget(); |
| const resourceModel = target.model(SDK.ResourceTreeModel); |
| if (resourceModel) { |
| resourceModel.reloadPage(); |
| } |
| } |
| this._reset(); |
| this._setState(Timeline.TimelinePanel.State.Recording); |
| this._showRecordingStarted(); |
| this._statusPane.updateStatus(Common.UIString('Profiling\u2026')); |
| this._statusPane.updateProgressBar(Common.UIString('Buffer usage'), 0); |
| this._statusPane.startTimer(); |
| this._hideLandingPage(); |
| } |
| |
| /** |
| * @override |
| * @param {number} usage |
| */ |
| recordingProgress(usage) { |
| this._statusPane.updateProgressBar(Common.UIString('Buffer usage'), usage * 100); |
| } |
| |
| _showLandingPage() { |
| if (this._landingPage) { |
| this._landingPage.show(this._statusPaneContainer); |
| return; |
| } |
| |
| /** |
| * @param {string} tagName |
| * @param {string} contents |
| */ |
| function encloseWithTag(tagName, contents) { |
| const e = createElement(tagName); |
| e.textContent = contents; |
| return e; |
| } |
| |
| const learnMoreNode = UI.XLink.create( |
| 'https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/', |
| Common.UIString('Learn\xa0more')); |
| |
| const recordKey = |
| encloseWithTag('b', UI.shortcutRegistry.shortcutDescriptorsForAction('timeline.toggle-recording')[0].name); |
| const reloadKey = |
| encloseWithTag('b', UI.shortcutRegistry.shortcutDescriptorsForAction('timeline.record-reload')[0].name); |
| const navigateNode = encloseWithTag('b', Common.UIString('WASD')); |
| |
| this._landingPage = new UI.VBox(); |
| this._landingPage.contentElement.classList.add('timeline-landing-page', 'fill'); |
| const centered = this._landingPage.contentElement.createChild('div'); |
| |
| const recordButton = UI.createInlineButton(UI.Toolbar.createActionButton(this._toggleRecordAction)); |
| const reloadButton = UI.createInlineButton(UI.Toolbar.createActionButtonForId('timeline.record-reload')); |
| |
| centered.createChild('p').appendChild(UI.formatLocalized( |
| 'Click the record button %s or hit %s to start a new recording.\nClick the reload button %s or hit %s to record the page load.', |
| [recordButton, recordKey, reloadButton, reloadKey])); |
| |
| centered.createChild('p').appendChild(UI.formatLocalized( |
| 'After recording, select an area of interest in the overview by dragging.\nThen, zoom and pan the timeline with the mousewheel or %s keys.\n%s', |
| [navigateNode, learnMoreNode])); |
| |
| this._landingPage.show(this._statusPaneContainer); |
| } |
| |
| _hideLandingPage() { |
| this._landingPage.detach(); |
| } |
| |
| /** |
| * @override |
| */ |
| loadingStarted() { |
| this._hideLandingPage(); |
| |
| if (this._statusPane) { |
| this._statusPane.hide(); |
| } |
| this._statusPane = new Timeline.TimelinePanel.StatusPane({showProgress: true}, this._cancelLoading.bind(this)); |
| this._statusPane.showPane(this._statusPaneContainer); |
| this._statusPane.updateStatus(Common.UIString('Loading profile\u2026')); |
| // 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.updateProgressBar(Common.UIString('Received'), progress * 100); |
| } |
| } |
| |
| /** |
| * @override |
| */ |
| processingStarted() { |
| this._statusPane.updateStatus(Common.UIString('Processing profile\u2026')); |
| } |
| |
| /** |
| * @override |
| * @param {?SDK.TracingModel} tracingModel |
| */ |
| loadingComplete(tracingModel) { |
| delete this._loader; |
| this._setState(Timeline.TimelinePanel.State.Idle); |
| |
| if (this._statusPane) { |
| this._statusPane.hide(); |
| } |
| delete this._statusPane; |
| |
| if (!tracingModel) { |
| this._clear(); |
| return; |
| } |
| |
| if (!this._performanceModel) { |
| this._performanceModel = new Timeline.PerformanceModel(); |
| } |
| this._performanceModel.setTracingModel(tracingModel); |
| this._setModel(this._performanceModel); |
| this._historyManager.addRecording(this._performanceModel); |
| |
| if (this._startCoverage.get()) { |
| UI.viewManager.showView('coverage') |
| .then(() => UI.viewManager.view('coverage').widget()) |
| .then(widget => widget.stopRecording()) |
| .then(() => this._updateOverviewControls()); |
| } |
| } |
| |
| _showRecordingStarted() { |
| if (this._statusPane) { |
| return; |
| } |
| this._statusPane = |
| new Timeline.TimelinePanel.StatusPane({showTimer: true, showProgress: true}, this._stopRecording.bind(this)); |
| this._statusPane.showPane(this._statusPaneContainer); |
| this._statusPane.updateStatus(Common.UIString('Initializing profiler\u2026')); |
| } |
| |
| _cancelLoading() { |
| if (this._loader) { |
| this._loader.cancel(); |
| } |
| } |
| |
| /** |
| * @param {!TimelineModel.TimelineModel} 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, Timeline.TimelineUIUtils.createEventDivider(event, zeroTime)); |
| } |
| this._overviewPane.setMarkers(markers); |
| } |
| |
| /** |
| * @param {!Common.Event} event |
| */ |
| async _loadEventFired(event) { |
| if (this._state !== Timeline.TimelinePanel.State.Recording || !this._recordingPageReload || |
| 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 !== Timeline.TimelinePanel.State.Recording) { |
| return; |
| } |
| this._stopRecording(); |
| } |
| |
| /** |
| * @param {!Timeline.TimelineSelection} selection |
| * @return {?TimelineModel.TimelineFrame} |
| */ |
| _frameForSelection(selection) { |
| switch (selection.type()) { |
| case Timeline.TimelineSelection.Type.Frame: |
| return /** @type {!TimelineModel.TimelineFrame} */ (selection.object()); |
| case Timeline.TimelineSelection.Type.Range: |
| return null; |
| case Timeline.TimelineSelection.Type.TraceEvent: |
| 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) { |
| 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 = Number.constrain(index + offset, 0, frames.length - 1); |
| const frame = frames[index]; |
| this._revealTimeRange(frame.startTime, frame.endTime); |
| this.select(Timeline.TimelineSelection.fromFrame(frame)); |
| return true; |
| } |
| |
| /** |
| * @override |
| * @param {?Timeline.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 = events.upperBound(time, (time, event) => time - event.startTime) - 1; index >= 0; --index) { |
| const event = events[index]; |
| const endTime = event.endTime || event.startTime; |
| if (SDK.TracingModel.isTopLevelEvent(event) && endTime < time) { |
| break; |
| } |
| if (this._performanceModel.isVisible(event) && endTime >= time) { |
| this.select(Timeline.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) { |
| 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(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} |
| */ |
| Timeline.TimelinePanel.State = { |
| Idle: Symbol('Idle'), |
| StartPending: Symbol('StartPending'), |
| Recording: Symbol('Recording'), |
| StopPending: Symbol('StopPending'), |
| Loading: Symbol('Loading'), |
| RecordingFailed: Symbol('RecordingFailed') |
| }; |
| |
| /** |
| * @enum {string} |
| */ |
| Timeline.TimelinePanel.ViewMode = { |
| FlameChart: 'FlameChart', |
| BottomUp: 'BottomUp', |
| CallTree: 'CallTree', |
| EventLog: 'EventLog' |
| }; |
| |
| // Define row and header height, should be in sync with styles for timeline graphs. |
| Timeline.TimelinePanel.rowHeight = 18; |
| Timeline.TimelinePanel.headerHeight = 20; |
| |
| /** @typedef {{selection: ?Timeline.TimelineSelection, windowLeftTime: number, windowRightTime: number}} */ |
| Timeline.TimelinePanel.ModelSelectionData; |
| |
| Timeline.TimelineSelection = class { |
| /** |
| * @param {!Timeline.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.TimelineFrame} frame |
| * @return {!Timeline.TimelineSelection} |
| */ |
| static fromFrame(frame) { |
| return new Timeline.TimelineSelection(Timeline.TimelineSelection.Type.Frame, frame.startTime, frame.endTime, frame); |
| } |
| |
| /** |
| * @param {!TimelineModel.TimelineModel.NetworkRequest} request |
| * @return {!Timeline.TimelineSelection} |
| */ |
| static fromNetworkRequest(request) { |
| return new Timeline.TimelineSelection( |
| Timeline.TimelineSelection.Type.NetworkRequest, request.startTime, request.endTime || request.startTime, |
| request); |
| } |
| |
| /** |
| * @param {!SDK.TracingModel.Event} event |
| * @return {!Timeline.TimelineSelection} |
| */ |
| static fromTraceEvent(event) { |
| return new Timeline.TimelineSelection( |
| Timeline.TimelineSelection.Type.TraceEvent, event.startTime, event.endTime || (event.startTime + 1), event); |
| } |
| |
| /** |
| * @param {number} startTime |
| * @param {number} endTime |
| * @return {!Timeline.TimelineSelection} |
| */ |
| static fromRange(startTime, endTime) { |
| return new Timeline.TimelineSelection(Timeline.TimelineSelection.Type.Range, startTime, endTime); |
| } |
| |
| /** |
| * @return {!Timeline.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} |
| */ |
| Timeline.TimelineSelection.Type = { |
| Frame: 'Frame', |
| NetworkRequest: 'NetworkRequest', |
| TraceEvent: 'TraceEvent', |
| Range: 'Range' |
| }; |
| |
| /** |
| * @interface |
| */ |
| Timeline.TimelineModeViewDelegate = function() {}; |
| |
| Timeline.TimelineModeViewDelegate.prototype = { |
| /** |
| * @param {?Timeline.TimelineSelection} selection |
| */ |
| select(selection) {}, |
| |
| /** |
| * @param {?Array<!SDK.TracingModel.Event>} events |
| * @param {number} time |
| */ |
| selectEntryAtTime(events, time) {}, |
| |
| /** |
| * @param {?SDK.TracingModel.Event} event |
| */ |
| highlightEvent(event) {}, |
| }; |
| |
| /** |
| * @unrestricted |
| */ |
| Timeline.TimelinePanel.StatusPane = class extends UI.VBox { |
| /** |
| * @param {!{showTimer: (boolean|undefined), showProgress: (boolean|undefined), description: (string|undefined)}} options - a collection of options controlling the appearance of the pane. |
| * The options object can have the following properties: |
| * - **showTimer** - `{boolean}` - Display seconds since dialog opened |
| * - **showProgress** - `{boolean}` - Display a progress bar |
| * - **description** - `{string}` - Display this string in a description line |
| * @param {function()} stopCallback |
| */ |
| constructor(options, stopCallback) { |
| super(true); |
| this.registerRequiredCSS('timeline/timelineStatusDialog.css'); |
| this.contentElement.classList.add('timeline-status-dialog'); |
| |
| const statusLine = this.contentElement.createChild('div', 'status-dialog-line status'); |
| statusLine.createChild('div', 'label').textContent = Common.UIString('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 = Common.UIString('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 = ls`Description`; |
| this._description = descriptionLine.createChild('div', 'content'); |
| this._description.innerText = options.description; |
| } |
| |
| this._stopButton = UI.createTextButton(Common.UIString('Stop'), stopCallback, '', true); |
| this.contentElement.createChild('div', 'stop-button').appendChild(this._stopButton); |
| } |
| |
| finish() { |
| this._stopTimer(); |
| this._stopButton.disabled = true; |
| } |
| |
| hide() { |
| this.element.parentNode.classList.remove('tinted'); |
| this.element.remove(); |
| } |
| |
| /** |
| * @param {!Element} parent |
| */ |
| showPane(parent) { |
| this.show(parent); |
| parent.classList.add('tinted'); |
| this._stopButton.focus(); |
| } |
| |
| /** |
| * @param {string} text |
| */ |
| updateStatus(text) { |
| this._status.textContent = text; |
| } |
| |
| /** |
| * @param {string} activity |
| * @param {number} percent |
| */ |
| updateProgressBar(activity, percent) { |
| this._progressLabel.textContent = activity; |
| this._progressBar.style.width = percent.toFixed(1) + '%'; |
| UI.ARIAUtils.setValueNow(this._progressBar, percent); |
| this._updateTimer(); |
| } |
| |
| /** |
| * @param {string} caption |
| */ |
| updateButton(caption) { |
| this._stopButton.innerText = caption; |
| } |
| |
| startTimer() { |
| this._startTime = Date.now(); |
| this._timeUpdateTimer = setInterval(this._updateTimer.bind(this, false), 1000); |
| this._updateTimer(); |
| } |
| |
| _stopTimer() { |
| if (!this._timeUpdateTimer) { |
| return; |
| } |
| clearInterval(this._timeUpdateTimer); |
| this._updateTimer(true); |
| delete this._timeUpdateTimer; |
| } |
| |
| /** |
| * @param {boolean=} precise |
| */ |
| _updateTimer(precise) { |
| if (!this._timeUpdateTimer) { |
| return; |
| } |
| const elapsed = (Date.now() - this._startTime) / 1000; |
| this._time.textContent = Common.UIString('%s\xa0sec', elapsed.toFixed(precise ? 1 : 0)); |
| } |
| }; |
| |
| |
| /** |
| * @implements {Common.QueryParamHandler} |
| * @unrestricted |
| */ |
| Timeline.LoadTimelineHandler = class { |
| /** |
| * @override |
| * @param {string} value |
| */ |
| handleQueryParam(value) { |
| UI.viewManager.showView('timeline').then(() => { |
| Timeline.TimelinePanel.instance()._loadFromURL(window.decodeURIComponent(value)); |
| }); |
| } |
| }; |
| |
| /** |
| * @implements {UI.ActionDelegate} |
| * @unrestricted |
| */ |
| Timeline.TimelinePanel.ActionDelegate = class { |
| /** |
| * @override |
| * @param {!UI.Context} context |
| * @param {string} actionId |
| * @return {boolean} |
| */ |
| handleAction(context, actionId) { |
| const panel = UI.context.flavor(Timeline.TimelinePanel); |
| console.assert(panel && panel instanceof Timeline.TimelinePanel); |
| switch (actionId) { |
| case 'timeline.toggle-recording': |
| panel._toggleRecording(); |
| return true; |
| case 'timeline.record-reload': |
| panel._recordReload(); |
| return true; |
| case 'timeline.save-to-file': |
| 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': |
| panel._showHistory(); |
| return true; |
| case 'timeline.previous-recording': |
| panel._navigateHistory(1); |
| return true; |
| case 'timeline.next-recording': |
| panel._navigateHistory(-1); |
| return true; |
| } |
| return false; |
| } |
| }; |
| |
| Timeline.TimelinePanel._traceProviderSettingSymbol = Symbol('traceProviderSetting'); |