blob: 6c3830e6c64b769e8a976d497392a79871ef319e [file] [log] [blame]
/*
* 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');