mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-13 01:54:52 +08:00
For all classes that were retrieved via Runtime.sharedInstance, migrate them to the singletong instance pattern. This removes the indirection via Runtime and improves code navigation as well as typechecking. R=jacktfranklin@chromium.org Bug: 1011811 Change-Id: Ifcf0b73be703308fbdbad0c585f0d945cee9b3b7 Fixed: 1131423 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2434340 Commit-Queue: Tim van der Lippe <tvanderlippe@chromium.org> Reviewed-by: Jack Franklin <jacktfranklin@chromium.org>
1374 lines
46 KiB
JavaScript
1374 lines
46 KiB
JavaScript
/*
|
|
* 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.
|
|
*/
|
|
// @ts-nocheck
|
|
// TODO(crbug.com/1011811): Enable TypeScript compiler checks
|
|
|
|
import * as Bindings from '../bindings/bindings.js';
|
|
import * as Common from '../common/common.js';
|
|
import * as Extensions from '../extensions/extensions.js';
|
|
import * as Host from '../host/host.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';
|
|
|
|
/** @type {!TimelinePanel} */
|
|
let timelinePanelInstance;
|
|
|
|
/**
|
|
* @implements {Client}
|
|
* @implements {TimelineModeViewDelegate}
|
|
* @unrestricted
|
|
*/
|
|
export class TimelinePanel extends UI.Panel.Panel {
|
|
constructor() {
|
|
super('timeline');
|
|
this.registerRequiredCSS('timeline/timelinePanel.css');
|
|
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],
|
|
Common.UIString.UIString('Drop timeline file or URL here'), this._handleDrop.bind(this));
|
|
|
|
/** @type {!Array<!UI.Toolbar.ToolbarItem>} */
|
|
this._recordingOptionUIControls = [];
|
|
this._state = State.Idle;
|
|
this._recordingPageReload = false;
|
|
this._millisecondsToRecordAfterLoadEvent = 5000;
|
|
this._toggleRecordAction =
|
|
/** @type {!UI.Action.Action }*/ (
|
|
UI.ActionRegistry.ActionRegistry.instance().action('timeline.toggle-recording'));
|
|
this._recordReloadAction =
|
|
/** @type {!UI.Action.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(Common.UIString.UIString('Disable JavaScript samples'));
|
|
this._captureLayersAndPicturesSetting =
|
|
Common.Settings.Settings.instance().createSetting('timelineCaptureLayersAndPictures', false);
|
|
this._captureLayersAndPicturesSetting.setTitle(
|
|
Common.UIString.UIString('Enable advanced paint instrumentation (slow)'));
|
|
|
|
this._showScreenshotsSetting = Common.Settings.Settings.instance().createSetting('timelineShowScreenshots', true);
|
|
this._showScreenshotsSetting.setTitle(Common.UIString.UIString('Screenshots'));
|
|
this._showScreenshotsSetting.addChangeListener(this._updateOverviewControls, this);
|
|
|
|
this._startCoverage = Common.Settings.Settings.instance().createSetting('timelineStartCoverage', false);
|
|
this._startCoverage.setTitle(ls`Coverage`);
|
|
|
|
if (!Root.Runtime.experiments.isEnabled('recordCoverageWithPerformanceTracing')) {
|
|
this._startCoverage.set(false);
|
|
}
|
|
|
|
this._showMemorySetting = Common.Settings.Settings.instance().createSetting('timelineShowMemory', false);
|
|
this._showMemorySetting.setTitle(Common.UIString.UIString('Memory'));
|
|
this._showMemorySetting.addChangeListener(this._onModeChanged, this);
|
|
|
|
const timelineToolbarContainer = this.element.createChild('div', 'timeline-toolbar-container');
|
|
this._panelToolbar = new UI.Toolbar.Toolbar('timeline-main-toolbar', timelineToolbarContainer);
|
|
this._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);
|
|
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._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);
|
|
}
|
|
|
|
/**
|
|
* @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) {
|
|
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(Common.UIString.UIString('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(Common.UIString.UIString('Load profile…'), 'largeicon-load');
|
|
this._loadButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, () => this._selectFileToLoad());
|
|
this._saveButton =
|
|
new UI.Toolbar.ToolbarButton(Common.UIString.UIString('Save profile…'), '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, Common.UIString.UIString('Capture screenshots'));
|
|
this._panelToolbar.appendToolbarItem(this._showScreenshotsToolbarCheckbox);
|
|
|
|
this._showMemoryToolbarCheckbox =
|
|
this._createSettingCheckbox(this._showMemorySetting, Common.UIString.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.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', Common.UIString.UIString('Capture settings'));
|
|
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,
|
|
Common.UIString.UIString(
|
|
'Disables JavaScript sampling, reduces overhead when running against mobile devices')));
|
|
captureToolbar.appendToolbarItem(this._createSettingCheckbox(
|
|
this._captureLayersAndPicturesSetting,
|
|
Common.UIString.UIString(
|
|
'Captures advanced paint instrumentation, introduces significant performance overhead')));
|
|
|
|
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(Common.UIString.UIString('Network:'));
|
|
this._networkThrottlingSelect = this._createNetworkConditionsSelect();
|
|
networkThrottlingToolbar.appendToolbarItem(this._networkThrottlingSelect);
|
|
|
|
const cpuThrottlingToolbar = new UI.Toolbar.Toolbar('', throttlingPane.element);
|
|
cpuThrottlingToolbar.appendText(Common.UIString.UIString('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 = traceProvider[traceProviderSettingSymbol];
|
|
if (!setting) {
|
|
const providerId = traceProvider.persistentIdentifier();
|
|
setting = Common.Settings.Settings.instance().createSetting(providerId, false);
|
|
setting.setTitle(traceProvider.shortDisplayName());
|
|
traceProvider[traceProviderSettingSymbol] = setting;
|
|
}
|
|
return setting;
|
|
}
|
|
|
|
/**
|
|
* @return {!UI.Toolbar.ToolbarComboBox}
|
|
*/
|
|
_createNetworkConditionsSelect() {
|
|
const toolbarItem = new UI.Toolbar.ToolbarComboBox(null, ls`Network conditions`);
|
|
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();
|
|
}
|
|
|
|
/**
|
|
* @suppress {deprecated}
|
|
*/
|
|
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 = await performanceModel.save(stream);
|
|
if (!error) {
|
|
return;
|
|
}
|
|
Common.Console.Console.instance().error(
|
|
Common.UIString.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 !== 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);
|
|
}
|
|
|
|
_updateSettingsPaneVisibility() {
|
|
if (this._showSettingsPaneSetting.get()) {
|
|
this._settingsPane.showWidget();
|
|
} else {
|
|
this._settingsPane.hideWidget();
|
|
}
|
|
}
|
|
|
|
_updateShowSettingsToolbarButton() {
|
|
const messages = [];
|
|
if (MobileThrottling.ThrottlingManager.throttlingManager().cpuThrottlingRate() !== 1) {
|
|
messages.push(Common.UIString.UIString('- CPU throttling is enabled'));
|
|
}
|
|
if (SDK.NetworkManager.MultitargetNetworkManager.instance().isThrottling()) {
|
|
messages.push(Common.UIString.UIString('- Network throttling is enabled'));
|
|
}
|
|
if (this._captureLayersAndPicturesSetting.get()) {
|
|
messages.push(Common.UIString.UIString('- Significant overhead due to paint instrumentation'));
|
|
}
|
|
if (this._disableCaptureJSProfileSetting.get()) {
|
|
messages.push(Common.UIString.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.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(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(() => UI.ViewManager.ViewManager.instance().view('coverage').widget())
|
|
.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);
|
|
if (response[ProtocolClient.InspectorBackend.ProtocolError]) {
|
|
this._recordingFailed(response[ProtocolClient.InspectorBackend.ProtocolError]);
|
|
} else {
|
|
this._recordingStarted();
|
|
}
|
|
}
|
|
|
|
async _stopRecording() {
|
|
if (this._statusPane) {
|
|
this._statusPane.finish();
|
|
this._statusPane.updateStatus(Common.UIString.UIString('Stopping timeline…'));
|
|
this._statusPane.updateProgressBar(Common.UIString.UIString('Received'), 0);
|
|
}
|
|
this._setState(State.StopPending);
|
|
if (this._startCoverage.get()) {
|
|
await UI.ViewManager.ViewManager.instance()
|
|
.showView('coverage')
|
|
.then(() => UI.ViewManager.ViewManager.instance().view('coverage').widget())
|
|
.then(widget => widget.stopRecording());
|
|
}
|
|
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: ls`Close`, buttonDisabled: false}, () => this.loadingComplete(null));
|
|
this._statusPane.showPane(this._statusPaneContainer);
|
|
this._statusPane.updateStatus(ls`Recording failed`);
|
|
|
|
this._setState(State.RecordingFailed);
|
|
this._performanceModel = null;
|
|
this._setUIControlsEnabled(true);
|
|
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 && !!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.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) {
|
|
const target = this._controller.mainTarget();
|
|
const resourceModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
|
|
if (resourceModel) {
|
|
resourceModel.reloadPage();
|
|
}
|
|
}
|
|
this._reset();
|
|
this._setState(State.Recording);
|
|
this._showRecordingStarted();
|
|
this._statusPane.enableAndFocusButton();
|
|
this._statusPane.updateStatus(Common.UIString.UIString('Profiling…'));
|
|
this._statusPane.updateProgressBar(Common.UIString.UIString('Buffer usage'), 0);
|
|
this._statusPane.startTimer();
|
|
this._hideLandingPage();
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {number} usage
|
|
*/
|
|
recordingProgress(usage) {
|
|
this._statusPane.updateProgressBar(Common.UIString.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.XLink.create(
|
|
'https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/',
|
|
Common.UIString.UIString('Learn\xa0more'));
|
|
|
|
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', Common.UIString.UIString('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(UI.UIUtils.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.UIUtils.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 StatusPane({showProgress: true}, this._cancelLoading.bind(this));
|
|
this._statusPane.showPane(this._statusPaneContainer);
|
|
this._statusPane.updateStatus(Common.UIString.UIString('Loading profile…'));
|
|
// 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.UIString('Received'), progress * 100);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
processingStarted() {
|
|
this._statusPane.updateStatus(Common.UIString.UIString('Processing profile…'));
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {?SDK.TracingModel.TracingModel} tracingModel
|
|
*/
|
|
loadingComplete(tracingModel) {
|
|
delete this._loader;
|
|
this._setState(State.Idle);
|
|
|
|
if (this._statusPane) {
|
|
this._statusPane.hide();
|
|
}
|
|
delete this._statusPane;
|
|
|
|
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(() => UI.ViewManager.ViewManager.instance().view('coverage').widget())
|
|
.then(widget => widget.processBacklog())
|
|
.then(() => this._updateOverviewControls());
|
|
}
|
|
}
|
|
|
|
_showRecordingStarted() {
|
|
if (this._statusPane) {
|
|
return;
|
|
}
|
|
this._statusPane =
|
|
new StatusPane({showTimer: true, showProgress: true, buttonDisabled: true}, this._stopRecording.bind(this));
|
|
this._statusPane.showPane(this._statusPaneContainer);
|
|
this._statusPane.updateStatus(Common.UIString.UIString('Initializing profiler…'));
|
|
}
|
|
|
|
_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.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:
|
|
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 = 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 = 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.TracingModel.isTopLevelEvent(event) && endTime < time) {
|
|
break;
|
|
}
|
|
if (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) {
|
|
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) {
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @unrestricted
|
|
*/
|
|
export class StatusPane extends UI.Widget.VBox {
|
|
/**
|
|
* @param {!{showTimer: (boolean|undefined), showProgress: (boolean|undefined), description: (string|undefined), buttonText: (string|undefined), buttonDisabled: (boolean|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
|
|
* - **buttonText** - `{string}` - The localized text to display on the button
|
|
* - **buttonDisabled** - `{string}` - Whether the button starts disabled or not - defaults to true
|
|
* @param {function(): undefined|function(): !Promise<undefined>} buttonCallback
|
|
*/
|
|
constructor(options, buttonCallback) {
|
|
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.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.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;
|
|
}
|
|
|
|
const buttonText = options.buttonText || ls`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() {
|
|
this._stopTimer();
|
|
this._button.disabled = true;
|
|
}
|
|
|
|
hide() {
|
|
this.element.parentNode.classList.remove('tinted');
|
|
this.element.remove();
|
|
}
|
|
|
|
/**
|
|
* @param {!Element} parent
|
|
*/
|
|
showPane(parent) {
|
|
this.show(parent);
|
|
parent.classList.add('tinted');
|
|
}
|
|
|
|
enableAndFocusButton() {
|
|
this._button.disabled = false;
|
|
this._button.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();
|
|
}
|
|
|
|
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.UIString('%s\xa0sec', elapsed.toFixed(precise ? 1 : 0));
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @implements {Common.QueryParamHandler.QueryParamHandler}
|
|
* @unrestricted
|
|
*/
|
|
export class LoadTimelineHandler {
|
|
/**
|
|
* @override
|
|
* @param {string} value
|
|
*/
|
|
handleQueryParam(value) {
|
|
UI.ViewManager.ViewManager.instance().showView('timeline').then(() => {
|
|
TimelinePanel.instance()._loadFromURL(window.decodeURIComponent(value));
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @implements {UI.ActionDelegate.ActionDelegate}
|
|
* @unrestricted
|
|
*/
|
|
export class ActionDelegate {
|
|
/**
|
|
* @override
|
|
* @param {!UI.Context.Context} context
|
|
* @param {string} actionId
|
|
* @return {boolean}
|
|
*/
|
|
handleAction(context, actionId) {
|
|
const panel = UI.Context.Context.instance().flavor(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':
|
|
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;
|
|
}
|
|
}
|
|
|
|
export const traceProviderSettingSymbol = Symbol('traceProviderSetting');
|