@quick-game/cli
Version:
Command line interface for rapid qg development
1,044 lines • 68.1 kB
JavaScript
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/*
* Copyright (C) 2012 Google Inc. All rights reserved.
* Copyright (C) 2012 Intel Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import * as Common from '../../core/common/common.js';
import * as Host from '../../core/host/host.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as Root from '../../core/root/root.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as TraceEngine from '../../models/trace/trace.js';
import * as PanelFeedback from '../../ui/components/panel_feedback/panel_feedback.js';
import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as MobileThrottling from '../mobile_throttling/mobile_throttling.js';
import historyToolbarButtonStyles from './historyToolbarButton.css.js';
import { Events, PerformanceModel } from './PerformanceModel.js';
import { cpuprofileJsonGenerator, traceJsonGenerator } from './SaveFileFormatter.js';
import { TimelineController } from './TimelineController.js';
import { TimelineFlameChartView } from './TimelineFlameChartView.js';
import { TimelineHistoryManager } from './TimelineHistoryManager.js';
import { TimelineLoader } from './TimelineLoader.js';
import { TimelineMiniMap } from './TimelineMiniMap.js';
import timelinePanelStyles from './timelinePanel.css.js';
import { TimelineSelection } from './TimelineSelection.js';
import timelineStatusDialogStyles from './timelineStatusDialog.css.js';
import { TimelineUIUtils } from './TimelineUIUtils.js';
import { UIDevtoolsController } from './UIDevtoolsController.js';
import { UIDevtoolsUtils } from './UIDevtoolsUtils.js';
const UIStrings = {
/**
*@description Text that appears when user drag and drop something (for example, a file) in Timeline Panel of the Performance panel
*/
dropTimelineFileOrUrlHere: 'Drop timeline file or URL here',
/**
*@description Title of disable capture jsprofile setting in timeline panel of the performance panel
*/
disableJavascriptSamples: 'Disable JavaScript samples',
/**
*@description Title of capture layers and pictures setting in timeline panel of the performance panel
*/
enableAdvancedPaint: 'Enable advanced paint instrumentation (slow)',
/**
*@description Title of show screenshots setting in timeline panel of the performance panel
*/
screenshots: 'Screenshots',
/**
*@description Text for the memory of the page
*/
memory: 'Memory',
/**
*@description Text to clear content
*/
clear: 'Clear',
/**
*@description Tooltip text that appears when hovering over the largeicon load button
*/
loadProfile: 'Load profile…',
/**
*@description Tooltip text that appears when hovering over the largeicon download button
*/
saveProfile: 'Save profile…',
/**
*@description Text to take screenshots
*/
captureScreenshots: 'Capture screenshots',
/**
*@description Text in Timeline Panel of the Performance panel
*/
showMemoryTimeline: 'Show memory timeline',
/**
*@description Tooltip text that appears when hovering over the largeicon settings gear in show settings pane setting in timeline panel of the performance panel
*/
captureSettings: 'Capture settings',
/**
*@description Text in Timeline Panel of the Performance panel
*/
disablesJavascriptSampling: 'Disables JavaScript sampling, reduces overhead when running against mobile devices',
/**
*@description Text in Timeline Panel of the Performance panel
*/
capturesAdvancedPaint: 'Captures advanced paint instrumentation, introduces significant performance overhead',
/**
*@description Text in Timeline Panel of the Performance panel
*/
network: 'Network:',
/**
*@description Text in Timeline Panel of the Performance panel
*/
cpu: 'CPU:',
/**
*@description Title of the 'Network conditions' tool in the bottom drawer
*/
networkConditions: 'Network conditions',
/**
*@description Text in Timeline Panel of the Performance panel
*@example {wrong format} PH1
*@example {ERROR_FILE_NOT_FOUND} PH2
*/
failedToSaveTimelineSS: 'Failed to save timeline: {PH1} ({PH2})',
/**
*@description Text in Timeline Panel of the Performance panel
*/
CpuThrottlingIsEnabled: '- CPU throttling is enabled',
/**
*@description Text in Timeline Panel of the Performance panel
*/
NetworkThrottlingIsEnabled: '- Network throttling is enabled',
/**
*@description Text in Timeline Panel of the Performance panel
*/
HardwareConcurrencyIsEnabled: '- Hardware concurrency override is enabled',
/**
*@description Text in Timeline Panel of the Performance panel
*/
SignificantOverheadDueToPaint: '- Significant overhead due to paint instrumentation',
/**
*@description Text in Timeline Panel of the Performance panel
*/
JavascriptSamplingIsDisabled: '- JavaScript sampling is disabled',
/**
*@description Text in Timeline Panel of the Performance panel
*/
stoppingTimeline: 'Stopping timeline…',
/**
*@description Text in Timeline Panel of the Performance panel
*/
received: 'Received',
/**
*@description Text to close something
*/
close: 'Close',
/**
*@description Status text to indicate the recording has failed in the Performance panel
*/
recordingFailed: 'Recording failed',
/**
* @description Text to indicate the progress of a profile. Informs the user that we are currently
* creating a peformance profile.
*/
profiling: 'Profiling…',
/**
*@description Text in Timeline Panel of the Performance panel
*/
bufferUsage: 'Buffer usage',
/**
*@description Text for an option to learn more about something
*/
learnmore: 'Learn more',
/**
*@description Text in Timeline Panel of the Performance panel
*/
wasd: 'WASD',
/**
*@description Text in Timeline Panel of the Performance panel
*@example {record} PH1
*@example {Ctrl + R} PH2
*/
clickTheRecordButtonSOrHitSTo: 'Click the record button {PH1} or hit {PH2} to start a new recording.',
/**
* @description Text in Timeline Panel of the Performance panel
* @example {reload button} PH1
* @example {Ctrl + R} PH2
*/
clickTheReloadButtonSOrHitSTo: 'Click the reload button {PH1} or hit {PH2} to record the page load.',
/**
*@description Text in Timeline Panel of the Performance panel
*@example {Ctrl + U} PH1
*@example {Learn more} PH2
*/
afterRecordingSelectAnAreaOf: 'After recording, select an area of interest in the overview by dragging. Then, zoom and pan the timeline with the mousewheel or {PH1} keys. {PH2}',
/**
*@description Text in Timeline Panel of the Performance panel
*/
loadingProfile: 'Loading profile…',
/**
*@description Text in Timeline Panel of the Performance panel
*/
processingProfile: 'Processing profile…',
/**
*@description Text in Timeline Panel of the Performance panel
*/
initializingProfiler: 'Initializing profiler…',
/**
*@description Text for the status of something
*/
status: 'Status',
/**
*@description Text that refers to the time
*/
time: 'Time',
/**
*@description Text for the description of something
*/
description: 'Description',
/**
*@description Text of an item that stops the running task
*/
stop: 'Stop',
/**
*@description Time text content in Timeline Panel of the Performance panel
*@example {2.12} PH1
*/
ssec: '{PH1} sec',
};
const str_ = i18n.i18n.registerUIStrings('panels/timeline/TimelinePanel.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
let timelinePanelInstance;
let isNode;
export class TimelinePanel extends UI.Panel.Panel {
dropTarget;
recordingOptionUIControls;
state;
recordingPageReload;
millisecondsToRecordAfterLoadEvent;
toggleRecordAction;
recordReloadAction;
#historyManager;
performanceModel;
disableCaptureJSProfileSetting;
captureLayersAndPicturesSetting;
showScreenshotsSetting;
showMemorySetting;
panelToolbar;
panelRightToolbar;
timelinePane;
#minimapComponent = new TimelineMiniMap();
statusPaneContainer;
flameChart;
searchableViewInternal;
showSettingsPaneButton;
showSettingsPaneSetting;
settingsPane;
controller;
cpuProfiler;
clearButton;
loadButton;
saveButton;
statusPane;
landingPage;
loader;
showScreenshotsToolbarCheckbox;
showMemoryToolbarCheckbox;
networkThrottlingSelect;
cpuThrottlingSelect;
fileSelectorElement;
selection;
traceLoadStart;
primaryPageTargetPromiseCallback = (_target) => { };
// Note: this is technically unused, but we need it to define the promiseCallback function above.
primaryPageTargetPromise = new Promise(res => {
this.primaryPageTargetPromiseCallback = res;
});
#traceEngineModel;
// Tracks the index of the trace that the user is currently viewing.
#traceEngineActiveTraceIndex = -1;
constructor(fullTraceEngine = false) {
super('timeline');
this.#traceEngineModel = fullTraceEngine ? TraceEngine.TraceModel.Model.createWithAllHandlers() :
TraceEngine.TraceModel.Model.createWithRequiredHandlersForMigration();
this.element.addEventListener('contextmenu', this.contextMenu.bind(this), false);
this.dropTarget = new UI.DropTarget.DropTarget(this.element, [UI.DropTarget.Type.File, UI.DropTarget.Type.URI], i18nString(UIStrings.dropTimelineFileOrUrlHere), this.handleDrop.bind(this));
this.recordingOptionUIControls = [];
this.state = State.Idle;
this.recordingPageReload = false;
this.millisecondsToRecordAfterLoadEvent = 5000;
this.toggleRecordAction =
UI.ActionRegistry.ActionRegistry.instance().action('timeline.toggle-recording');
this.recordReloadAction =
UI.ActionRegistry.ActionRegistry.instance().action('timeline.record-reload');
this.#historyManager = new TimelineHistoryManager();
this.performanceModel = null;
this.traceLoadStart = null;
this.disableCaptureJSProfileSetting =
Common.Settings.Settings.instance().createSetting('timelineDisableJSSampling', false);
this.disableCaptureJSProfileSetting.setTitle(i18nString(UIStrings.disableJavascriptSamples));
this.captureLayersAndPicturesSetting =
Common.Settings.Settings.instance().createSetting('timelineCaptureLayersAndPictures', false);
this.captureLayersAndPicturesSetting.setTitle(i18nString(UIStrings.enableAdvancedPaint));
this.showScreenshotsSetting =
Common.Settings.Settings.instance().createSetting('timelineShowScreenshots', isNode ? false : true);
this.showScreenshotsSetting.setTitle(i18nString(UIStrings.screenshots));
this.showScreenshotsSetting.addChangeListener(this.updateOverviewControls, this);
this.showMemorySetting = Common.Settings.Settings.instance().createSetting('timelineShowMemory', false);
this.showMemorySetting.setTitle(i18nString(UIStrings.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.panelToolbar.makeWrappable(true);
this.panelRightToolbar = new UI.Toolbar.Toolbar('', timelineToolbarContainer);
if (!isNode) {
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';
this.#minimapComponent.show(topPaneElement);
this.#minimapComponent.addEventListener(PerfUI.TimelineOverviewPane.Events.WindowChanged, this.onOverviewWindowChanged.bind(this));
this.statusPaneContainer = this.timelinePane.element.createChild('div', 'status-pane-container fill');
this.createFileSelector();
SDK.TargetManager.TargetManager.instance().addModelListener(SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.Load, this.loadEventFired, this);
this.flameChart = new TimelineFlameChartView(this);
this.searchableViewInternal = new UI.SearchableView.SearchableView(this.flameChart, null);
this.searchableViewInternal.setMinimumSize(0, 100);
this.searchableViewInternal.element.classList.add('searchable-view');
this.searchableViewInternal.show(this.timelinePane.element);
this.flameChart.show(this.searchableViewInternal.element);
this.flameChart.setSearchableView(this.searchableViewInternal);
this.searchableViewInternal.hideWidget();
this.onModeChanged();
this.populateToolbar();
this.showLandingPage();
this.updateTimelineControls();
SDK.TargetManager.TargetManager.instance().addEventListener(SDK.TargetManager.Events.SuspendStateChanged, this.onSuspendStateChanged, this);
if (Root.Runtime.experiments.isEnabled('timelineAsConsoleProfileResultPanel')) {
const profilerModels = SDK.TargetManager.TargetManager.instance().models(SDK.CPUProfilerModel.CPUProfilerModel);
for (const model of profilerModels) {
for (const message of model.registeredConsoleProfileMessages) {
this.consoleProfileFinished(message);
}
}
SDK.TargetManager.TargetManager.instance().observeModels(SDK.CPUProfilerModel.CPUProfilerModel, {
modelAdded: (model) => {
model.addEventListener(SDK.CPUProfilerModel.Events.ConsoleProfileFinished, event => this.consoleProfileFinished(event.data));
},
modelRemoved: (_model) => {
},
});
}
SDK.TargetManager.TargetManager.instance().observeTargets({
targetAdded: (target) => {
if (target !== SDK.TargetManager.TargetManager.instance().primaryPageTarget()) {
return;
}
this.primaryPageTargetPromiseCallback(target);
},
targetRemoved: (_) => { },
});
}
static instance(opts = { forceNew: null, isNode: false, fullTraceEngine: false }) {
const { forceNew, isNode: isNodeMode } = opts;
isNode = isNodeMode;
if (!timelinePanelInstance || forceNew) {
timelinePanelInstance = new TimelinePanel(opts.fullTraceEngine);
}
return timelinePanelInstance;
}
searchableView() {
return this.searchableViewInternal;
}
wasShown() {
super.wasShown();
UI.Context.Context.instance().setFlavor(TimelinePanel, this);
this.registerCSSFiles([timelinePanelStyles]);
// Record the performance tool load time.
Host.userMetrics.panelLoaded('timeline', 'DevTools.Launch.Timeline');
}
willHide() {
UI.Context.Context.instance().setFlavor(TimelinePanel, null);
this.#historyManager.cancelIfShowing();
}
loadFromEvents(events) {
if (this.state !== State.Idle) {
return;
}
this.prepareToLoadTimeline();
this.loader = TimelineLoader.loadFromEvents(events, this);
}
loadFromCpuProfile(profile, title) {
if (this.state !== State.Idle) {
return;
}
this.prepareToLoadTimeline();
this.loader = TimelineLoader.loadFromCpuProfile(profile, this, title);
}
onOverviewWindowChanged(event) {
if (!this.performanceModel) {
return;
}
const left = (event.data.startTime > 0) ? event.data.startTime : this.performanceModel.minimumRecordTime();
const right = Number.isFinite(event.data.endTime) ? event.data.endTime : this.performanceModel.maximumRecordTime();
this.performanceModel.setWindow({ left, right }, /* animate */ true);
}
onModelWindowChanged(event) {
const window = event.data.window;
this.#minimapComponent.setWindowTimes(window.left, window.right);
}
setState(state) {
this.state = state;
this.updateTimelineControls();
}
createSettingCheckbox(setting, tooltip) {
const checkboxItem = new UI.Toolbar.ToolbarSettingCheckbox(setting, tooltip);
this.recordingOptionUIControls.push(checkboxItem);
return checkboxItem;
}
populateToolbar() {
// Record
this.panelToolbar.appendToolbarItem(UI.Toolbar.Toolbar.createActionButton(this.toggleRecordAction));
this.panelToolbar.appendToolbarItem(UI.Toolbar.Toolbar.createActionButton(this.recordReloadAction));
this.clearButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.clear), 'clear');
this.clearButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, () => this.onClearButton());
this.panelToolbar.appendToolbarItem(this.clearButton);
// Load / Save
this.loadButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.loadProfile), 'import');
this.loadButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, () => {
Host.userMetrics.actionTaken(Host.UserMetrics.Action.PerfPanelTraceImported);
this.selectFileToLoad();
});
this.saveButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.saveProfile), 'download');
this.saveButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, _event => {
Host.userMetrics.actionTaken(Host.UserMetrics.Action.PerfPanelTraceExported);
void this.saveToFile();
});
this.panelToolbar.appendSeparator();
this.panelToolbar.appendToolbarItem(this.loadButton);
this.panelToolbar.appendToolbarItem(this.saveButton);
// History
this.panelToolbar.appendSeparator();
this.panelToolbar.appendToolbarItem(this.#historyManager.button());
this.panelToolbar.registerCSSFiles([historyToolbarButtonStyles]);
this.panelToolbar.appendSeparator();
// View
this.panelToolbar.appendSeparator();
if (!isNode) {
this.showScreenshotsToolbarCheckbox =
this.createSettingCheckbox(this.showScreenshotsSetting, i18nString(UIStrings.captureScreenshots));
this.panelToolbar.appendToolbarItem(this.showScreenshotsToolbarCheckbox);
}
this.showMemoryToolbarCheckbox =
this.createSettingCheckbox(this.showMemorySetting, i18nString(UIStrings.showMemoryTimeline));
this.panelToolbar.appendToolbarItem(this.showMemoryToolbarCheckbox);
// GC
this.panelToolbar.appendToolbarItem(UI.Toolbar.Toolbar.createActionButtonForId('components.collect-garbage'));
// Settings
if (!isNode) {
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, 'gear', i18nString(UIStrings.captureSettings), 'gear-filled');
SDK.NetworkManager.MultitargetNetworkManager.instance().addEventListener(SDK.NetworkManager.MultitargetNetworkManager.Events.ConditionsChanged, this.updateShowSettingsToolbarButton, this);
SDK.CPUThrottlingManager.CPUThrottlingManager.instance().addEventListener(SDK.CPUThrottlingManager.Events.RateChanged, this.updateShowSettingsToolbarButton, this);
SDK.CPUThrottlingManager.CPUThrottlingManager.instance().addEventListener(SDK.CPUThrottlingManager.Events.HardwareConcurrencyChanged, this.updateShowSettingsToolbarButton, this);
this.disableCaptureJSProfileSetting.addChangeListener(this.updateShowSettingsToolbarButton, this);
this.captureLayersAndPicturesSetting.addChangeListener(this.updateShowSettingsToolbarButton, this);
this.settingsPane = new UI.Widget.HBox();
this.settingsPane.element.classList.add('timeline-settings-pane');
this.settingsPane.show(this.element);
const captureToolbar = new UI.Toolbar.Toolbar('', this.settingsPane.element);
captureToolbar.element.classList.add('flex-auto');
captureToolbar.makeVertical();
captureToolbar.appendToolbarItem(this.createSettingCheckbox(this.disableCaptureJSProfileSetting, i18nString(UIStrings.disablesJavascriptSampling)));
captureToolbar.appendToolbarItem(this.createSettingCheckbox(this.captureLayersAndPicturesSetting, i18nString(UIStrings.capturesAdvancedPaint)));
const throttlingPane = new UI.Widget.VBox();
throttlingPane.element.classList.add('flex-auto');
throttlingPane.show(this.settingsPane.element);
const cpuThrottlingToolbar = new UI.Toolbar.Toolbar('', throttlingPane.element);
cpuThrottlingToolbar.appendText(i18nString(UIStrings.cpu));
this.cpuThrottlingSelect = MobileThrottling.ThrottlingManager.throttlingManager().createCPUThrottlingSelector();
cpuThrottlingToolbar.appendToolbarItem(this.cpuThrottlingSelect);
const networkThrottlingToolbar = new UI.Toolbar.Toolbar('', throttlingPane.element);
networkThrottlingToolbar.appendText(i18nString(UIStrings.network));
this.networkThrottlingSelect = this.createNetworkConditionsSelect();
networkThrottlingToolbar.appendToolbarItem(this.networkThrottlingSelect);
const hardwareConcurrencyPane = new UI.Widget.VBox();
hardwareConcurrencyPane.element.classList.add('flex-auto');
hardwareConcurrencyPane.show(this.settingsPane.element);
const { toggle, input, reset, warning } = MobileThrottling.ThrottlingManager.throttlingManager().createHardwareConcurrencySelector();
const concurrencyThrottlingToolbar = new UI.Toolbar.Toolbar('', hardwareConcurrencyPane.element);
concurrencyThrottlingToolbar.registerCSSFiles([timelinePanelStyles]);
input.element.classList.add('timeline-concurrency-input');
concurrencyThrottlingToolbar.appendToolbarItem(toggle);
concurrencyThrottlingToolbar.appendToolbarItem(input);
concurrencyThrottlingToolbar.appendToolbarItem(reset);
concurrencyThrottlingToolbar.appendToolbarItem(warning);
this.showSettingsPaneSetting.addChangeListener(this.updateSettingsPaneVisibility.bind(this));
this.updateSettingsPaneVisibility();
}
createNetworkConditionsSelect() {
const toolbarItem = new UI.Toolbar.ToolbarComboBox(null, i18nString(UIStrings.networkConditions));
toolbarItem.setMaxWidth(140);
MobileThrottling.ThrottlingManager.throttlingManager().decorateSelectWithNetworkThrottling(toolbarItem.selectElement());
return toolbarItem;
}
prepareToLoadTimeline() {
console.assert(this.state === State.Idle);
this.setState(State.Loading);
if (this.performanceModel) {
this.performanceModel = null;
}
}
createFileSelector() {
if (this.fileSelectorElement) {
this.fileSelectorElement.remove();
}
this.fileSelectorElement = UI.UIUtils.createFileSelectorElement(this.loadFromFile.bind(this));
this.timelinePane.element.appendChild(this.fileSelectorElement);
}
contextMenu(event) {
const contextMenu = new UI.ContextMenu.ContextMenu(event);
contextMenu.appendItemsAtLocation('timelineMenu');
void contextMenu.show();
}
async saveToFile() {
if (this.state !== State.Idle) {
return;
}
const performanceModel = this.performanceModel;
if (!performanceModel) {
return;
}
const traceEvents = this.#traceEngineModel.traceEvents(this.#traceEngineActiveTraceIndex);
const metadata = this.#traceEngineModel.metadata(this.#traceEngineActiveTraceIndex);
if (!traceEvents) {
return;
}
const traceStart = Platform.DateUtilities.toISO8601Compact(new Date());
let fileName;
if (metadata?.dataOrigin === "CPUProfile" /* TraceEngine.Types.File.DataOrigin.CPUProfile */) {
fileName = `CPU-${traceStart}.cpuprofile`;
}
else {
fileName = `Trace-${traceStart}.json`;
}
try {
const handler = await window.showSaveFilePicker({
suggestedName: fileName,
});
const encoder = new TextEncoder();
// TODO(crbug.com/1456818): Extract this logic and add more tests.
let traceAsString;
if (metadata?.dataOrigin === "CPUProfile" /* TraceEngine.Types.File.DataOrigin.CPUProfile */) {
const profileEvent = traceEvents.find(e => e.name === 'CpuProfile');
if (!profileEvent || !profileEvent.args?.data) {
return;
}
const profileEventData = profileEvent.args?.data;
if (profileEventData.hasOwnProperty('cpuProfile')) {
// TODO(crbug.com/1456799): Currently use a hack way because we can't differentiate
// cpuprofile from trace events when loading a file.
// The loader will directly add the fake trace created from CpuProfile to the tracingModel.
// And there is where the old saving logic saves the cpuprofile.
// This will be solved when the CPUProfileHandler is done. Then we can directly get it
// from the new traceEngine
const profile = profileEventData.cpuProfile;
traceAsString = cpuprofileJsonGenerator(profile);
}
}
else {
const formattedTraceIter = traceJsonGenerator(traceEvents, metadata);
traceAsString = Array.from(formattedTraceIter).join('');
}
const buffer = encoder.encode(traceAsString);
const writable = await handler.createWritable();
await writable.write(buffer);
await writable.close();
}
catch (error) {
console.error(error.stack);
if (error.name === 'AbortError') {
// The user cancelled the action, so this is not an error we need to report.
return;
}
Common.Console.Console.instance().error(i18nString(UIStrings.failedToSaveTimelineSS, { PH1: error.message, PH2: error.name }));
}
}
async showHistory() {
const recordingData = await this.#historyManager.showHistoryDropDown();
if (recordingData && recordingData.legacyModel !== this.performanceModel) {
this.setModel(recordingData.legacyModel, /* exclusiveFilter= */ null, recordingData.traceParseDataIndex);
}
}
navigateHistory(direction) {
const recordingData = this.#historyManager.navigate(direction);
if (recordingData && recordingData.legacyModel !== this.performanceModel) {
this.setModel(recordingData.legacyModel, /* exclusiveFilter= */ null, recordingData.traceParseDataIndex);
}
return true;
}
selectFileToLoad() {
if (this.fileSelectorElement) {
this.fileSelectorElement.click();
}
}
async loadFromFile(file) {
if (this.state !== State.Idle) {
return;
}
this.prepareToLoadTimeline();
this.loader = await TimelineLoader.loadFromFile(file, this);
this.createFileSelector();
}
async loadFromURL(url) {
if (this.state !== State.Idle) {
return;
}
this.prepareToLoadTimeline();
this.loader = await TimelineLoader.loadFromURL(url, this);
}
updateOverviewControls() {
const traceParsedData = this.#traceEngineModel.traceParsedData(this.#traceEngineActiveTraceIndex);
this.#minimapComponent.setData({
performanceModel: this.performanceModel,
traceParsedData,
settings: {
showScreenshots: this.showScreenshotsSetting.get(),
showMemory: this.showMemorySetting.get(),
},
});
}
onModeChanged() {
this.updateOverviewControls();
this.doResize();
this.select(null);
}
updateSettingsPaneVisibility() {
if (isNode) {
return;
}
if (this.showSettingsPaneSetting.get()) {
this.settingsPane.showWidget();
}
else {
this.settingsPane.hideWidget();
}
}
updateShowSettingsToolbarButton() {
const messages = [];
if (SDK.CPUThrottlingManager.CPUThrottlingManager.instance().cpuThrottlingRate() !== 1) {
messages.push(i18nString(UIStrings.CpuThrottlingIsEnabled));
}
if (MobileThrottling.ThrottlingManager.throttlingManager().hardwareConcurrencyOverrideEnabled) {
messages.push(i18nString(UIStrings.HardwareConcurrencyIsEnabled));
}
if (SDK.NetworkManager.MultitargetNetworkManager.instance().isThrottling()) {
messages.push(i18nString(UIStrings.NetworkThrottlingIsEnabled));
}
if (this.captureLayersAndPicturesSetting.get()) {
messages.push(i18nString(UIStrings.SignificantOverheadDueToPaint));
}
if (this.disableCaptureJSProfileSetting.get()) {
messages.push(i18nString(UIStrings.JavascriptSamplingIsDisabled));
}
this.showSettingsPaneButton.setDefaultWithRedColor(messages.length > 0);
this.showSettingsPaneButton.setToggleWithRedColor(messages.length > 0);
if (messages.length) {
const tooltipElement = document.createElement('div');
messages.forEach(message => {
tooltipElement.createChild('div').textContent = message;
});
this.showSettingsPaneButton.setTitle(tooltipElement.textContent || '');
}
else {
this.showSettingsPaneButton.setTitle(i18nString(UIStrings.captureSettings));
}
}
setUIControlsEnabled(enabled) {
this.recordingOptionUIControls.forEach(control => control.setEnabled(enabled));
}
async #evaluateInspectedURL() {
if (!this.controller) {
return Platform.DevToolsPath.EmptyUrlString;
}
// target.inspectedURL is reliably populated, however it lacks any url #hash
const inspectedURL = this.controller.primaryPageTarget.inspectedURL();
// We'll use the navigationHistory to acquire the current URL including hash
const resourceTreeModel = this.controller.primaryPageTarget.model(SDK.ResourceTreeModel.ResourceTreeModel);
const navHistory = resourceTreeModel && await resourceTreeModel.navigationHistory();
if (!resourceTreeModel || !navHistory) {
return inspectedURL;
}
const { currentIndex, entries } = navHistory;
const navigationEntry = entries[currentIndex];
return navigationEntry.url;
}
async #navigateToAboutBlank() {
const aboutBlankNavigationComplete = new Promise(async (resolve, reject) => {
if (!this.controller) {
reject('Could not find TimelineController');
return;
}
const target = this.controller.primaryPageTarget;
const resourceModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
if (!resourceModel) {
reject('Could not load resourceModel');
return;
}
// To clear out the page and any state from prior test runs, we
// navigate to about:blank before initiating the trace recording.
// Once we have navigated to about:blank, we start recording and
// then navigate to the original page URL, to ensure we profile the
// page load.
function waitForAboutBlank(event) {
if (event.data.url === 'about:blank') {
resolve();
}
else {
reject(`Unexpected navigation to ${event.data.url}`);
}
resourceModel?.removeEventListener(SDK.ResourceTreeModel.Events.FrameNavigated, waitForAboutBlank);
}
resourceModel.addEventListener(SDK.ResourceTreeModel.Events.FrameNavigated, waitForAboutBlank);
await resourceModel.navigate('about:blank');
});
await aboutBlankNavigationComplete;
}
async #startCPUProfilingRecording() {
try {
// Only profile the first target devtools connects to. If we profile all target, but this will cause some bugs
// like time for the function is calculated wrong, because the profiles will be concated and sorted together,
// so the total time will be amplified.
// Multiple targets problem might happen when you inspect multiple node servers on different port at same time,
// or when you let DevTools listen to both locolhost:9229 & 127.0.0.1:9229.
const firstNodeTarget = SDK.TargetManager.TargetManager.instance().targets().find(target => target.type() === SDK.Target.Type.Node);
if (!firstNodeTarget) {
throw new Error('Could not load any Node target.');
}
if (firstNodeTarget) {
this.cpuProfiler = firstNodeTarget.model(SDK.CPUProfilerModel.CPUProfilerModel);
}
this.setUIControlsEnabled(false);
this.hideLandingPage();
if (!this.cpuProfiler) {
throw new Error('No Node target is found.');
}
await SDK.TargetManager.TargetManager.instance().suspendAllTargets('performance-timeline');
await this.cpuProfiler.startRecording();
this.recordingStarted();
}
catch (e) {
await this.recordingFailed(e.message);
}
}
async #startTraceRecording() {
try {
const primaryPageTarget = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
if (!primaryPageTarget) {
throw new Error('Could not load primary page target.');
}
if (UIDevtoolsUtils.isUiDevTools()) {
this.controller = new UIDevtoolsController(primaryPageTarget, this);
}
else {
this.controller = new TimelineController(primaryPageTarget, this);
}
this.setUIControlsEnabled(false);
this.hideLandingPage();
if (!this.controller) {
throw new Error('Could not create Timeline controller');
}
const urlToTrace = await this.#evaluateInspectedURL();
// If we are doing "Reload & record", we first navigate the page to
// about:blank. This is to ensure any data on the timeline from any
// previous performance recording is lost, avoiding the problem where a
// timeline will show data & screenshots from a previous page load that
// was not relevant.
if (this.recordingPageReload) {
await this.#navigateToAboutBlank();
}
const recordingOptions = {
enableJSSampling: !this.disableCaptureJSProfileSetting.get(),
capturePictures: this.captureLayersAndPicturesSetting.get(),
captureFilmStrip: this.showScreenshotsSetting.get(),
};
// Order is important here: we tell the controller to start recording, which enables tracing.
const response = await this.controller.startRecording(recordingOptions);
if (response.getError()) {
throw new Error(response.getError());
}
// Once we get here, we know tracing is active.
// This is when, if the user has hit "Reload & Record" that we now need to navigate to the original URL.
// If the user has just hit "record", we don't do any navigating.
const recordingConfig = this.recordingPageReload ? { navigateToUrl: urlToTrace } : undefined;
this.recordingStarted(recordingConfig);
}
catch (e) {
await this.recordingFailed(e.message);
}
}
async startRecording() {
console.assert(!this.statusPane, 'Status pane is already opened.');
this.setState(State.StartPending);
this.showRecordingStarted();
if (isNode) {
await this.#startCPUProfilingRecording();
}
else {
await this.#startTraceRecording();
}
}
async stopRecording() {
if (this.statusPane) {
this.statusPane.finish();
this.statusPane.updateStatus(i18nString(UIStrings.stoppingTimeline));
this.statusPane.updateProgressBar(i18nString(UIStrings.received), 0);
}
this.setState(State.StopPending);
if (this.controller) {
this.performanceModel = this.controller.getPerformanceModel();
await this.controller.stopRecording();
this.setUIControlsEnabled(true);
await this.controller.dispose();
this.controller = null;
return;
}
if (this.cpuProfiler) {
const profile = await this.cpuProfiler.stopRecording();
this.setState(State.Idle);
this.loadFromCpuProfile(profile);
this.setUIControlsEnabled(true);
this.cpuProfiler = null;
await SDK.TargetManager.TargetManager.instance().resumeAllTargets();
}
}
async recordingFailed(error) {
if (this.statusPane) {
this.statusPane.remove();
}
this.statusPane = new StatusPane({
description: error,
buttonText: i18nString(UIStrings.close),
buttonDisabled: false,
showProgress: undefined,
showTimer: undefined,
},
// When recording failed, we should load null to go back to the landing page.
() => this.loadingComplete(/* tracingModel= */ null, /* exclusiveFilter= */ null, /* isCpuProfile= */ false));
this.statusPane.showPane(this.statusPaneContainer);
this.statusPane.updateStatus(i18nString(UIStrings.recordingFailed));
this.setState(State.RecordingFailed);
this.performanceModel = null;
this.traceLoadStart = null;
this.setUIControlsEnabled(true);
if (this.controller) {
await this.controller.dispose();
this.controller = null;
}
// Ensure we resume all targets, otherwise DevTools remains unresponsive in the event of an error.
void SDK.TargetManager.TargetManager.instance().resumeAllTargets();
}
onSuspendStateChanged() {
this.updateTimelineControls();
}
consoleProfileFinished(data) {
this.loadFromCpuProfile(data.cpuProfile, data.title);
void UI.InspectorView.InspectorView.instance().showPanel('timeline');
}
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(isNode ? false : this.state === state.Idle);
this.#historyManager.setEnabled(this.state === state.Idle);
this.clearButton.setEnabled(this.state === state.Idle);
this.panelToolbar.setEnabled(this.state !== state.Loading);
this.panelRightToolbar.setEnabled(this.state !== state.Loading);
this.dropTarget.setEnabled(this.state === state.Idle);
this.loadButton.setEnabled(this.state === state.Idle);
this.saveButton.setEnabled(this.state === state.Idle && Boolean(this.performanceModel));
}
async toggleRecording() {
if (this.state === State.Idle) {
this.recordingPageReload = false;
await this.startRecording();
Host.userMetrics.actionTaken(Host.UserMetrics.Action.TimelineStarted);
}
else if (this.state === State.Recording) {
await this.stopRecording();
}
}
recordReload() {
if (this.state !== State.Idle) {
return;
}
this.recordingPageReload = true;
void this.startRecording();
Host.userMetrics.actionTaken(Host.UserMetrics.Action.TimelinePageReloadStarted);
}
onClearButton() {
this.#historyManager.clear();
this.clear();
}
clear() {
if (this.statusPane) {
this.statusPane.remove();
}
this.showLandingPage();
this.reset();
}
reset() {
PerfUI.LineLevelProfile.Performance.instance().reset();
if (this.performanceModel) {
this.performanceModel.removeEventListener(Events.NamesResolved, this.updateModelAndFlameChart, this);
}
this.setModel(null);
}
applyFilters(model, exclusiveFilter = null) {
if (model.timelineModel().isGenericTrace() || Root.Runtime.experiments.isEnabled('timelineShowAllEvents')) {
return;
}
model.setFilters(exclusiveFilter ? [exclusiveFilter] : [TimelineUIUtils.visibleEventsFilter()]);
}
setModel(model, exclusiveFilter = null, traceEngineIndex = -1) {
if (this.performanceModel) {
this.performanceModel.removeEventListener(Events.WindowChanged, this.onModelWindowChanged, this);
}
this.performanceModel = model;
if (model) {
this.searchableViewInternal.showWidget();
this.applyFilters(model, exclusiveFilter);
}
else {
this.searchableViewInternal.hideWidget();
}
this.#traceEngineActiveTraceIndex = traceEngineIndex;
const traceParsedData = this.#traceEngineModel.traceParsedData(this.#traceEngineActiveTraceIndex);
this.flameChart.setModel(model, traceParsedData);
this.updateOverviewControls();
this.#minimapComponent.reset();
if (model && this.performanceModel) {
this.performanceModel.addEventListener(Events.WindowChanged, this.onModelWindowChanged, this);
this.#minimapComponent.setBounds(TraceEngine.Types.Timing.MilliSeconds(model.timelineModel().minimumRecordTime()), TraceEngine.Types.Timing.MilliSeconds(model.timelineModel().maximumRecordTime()));
PerfUI.LineLevelProfile.Performance.instance().reset();
for (const profile of model.timelineModel().cpuProfiles()) {
PerfUI.LineLevelProfile.Performance.instance().appendCPUProfile(profile.cpuProfileData, profile.target);
}
this.flameChart.setSelection(null);
this.#minimapComponent.setWindowTimes(model.window().left, model.window().right);
}
this.updateOverviewControls();
if (this.flameChart) {
this.flameChart.resizeToPreferredHeights();
}
this.updateTimelineControls();
}
recordingStarted(config) {
if (config && this.recordingPageReload && this.controller) {
// If the user hit "Reload & record", by this point we have:
// 1. Navigated to about:blank
// 2. Initiated tracing.
// We therefore now should navigate back to the original URL that the user wants to profile.
const resourceModel = this.controller?.primaryPageTarget.model(SDK.ResourceTreeModel.ResourceTreeModel);
if (!resourceModel) {
void this.recordingFailed('Could not navigate to original URL');
return;
}
// We don't need to await this because we are purposefully showing UI
// progress as the page loads & tracing is underway.
void resourceModel.navigate(config.navigateToUrl);
}
this.reset();
this.setState(State.Recording);
this.showRecordingStarted();
if (this.statusPane) {
this.statusPane.enableAndFocusButton();
this.statusPane.updateStatus(i18nString(UIStrings.profiling));
this.statusPane.updateProgressBar(i18nString(UIStrings.bufferUsage), 0);
this.statusPane.startTimer();
}
this.hideLandingPage();
}
recordingProgress(usage) {
if (this.statusPane) {
this.statusPane.updateProgressBar(i18nString(UIStrings.bufferUsage), usage * 100);
}
}
showLandingPage() {
this.updateSettingsPaneVisibility();
if (this.landingPage) {
this.landingPage.show(this.statusPaneContainer);
return;
}
function encloseWithTag(tagName, contents) {
const e = document.createElement(tagName);
e.textContent = contents;
return e;
}
const learnMoreNode = UI.XLink.XLink.create('https://developer.chrome.com/docs/devtools/evaluate-performance/', i18nString(UIStrings.learnmore));
const recordKey = encloseWithTag('b', UI.ShortcutRegistry.ShortcutRegistry.instance().shortcutsForAction('timeline.toggle-recording')[0].title());
const reloadKey = encloseWithTag('b', UI.ShortcutRegistry.ShortcutRegistry.instance().shortcutsForAction('timeline.record-reload')[0].title());
const navigateNode = encloseWithTag('b', i18nString(UIStrings.wasd));
this.landingPage = new UI.Widget.VBox();
this.landingPage.contentElement.classList.add('timeline-landing-page', 'fill');
const centered = this.landingPage.contentElement.createChild('div');
const recordButton = UI.UIUtils.createInlineButton(UI.Toolbar.Toolbar.createActionButton(this.toggleRecordAction));
const reloadButton = UI.UIUtils.createInlineButton(UI.Toolbar.Toolbar.createActionButtonForId('timeline.record-reload'));
centered.createChild('p').appendChild(i18n.i18n.getFormatLocalizedString(str_, UIStrings.clickTheRecordButtonSOrHitSTo, { PH1: recordButton, PH2: recordKey }));
centered.createChild('p').appendChild(i18n.i18n.getFormatLocalizedString(str_, UIStrings.clickTheReloadButtonSOrHitSTo, { PH1: reloadButton, PH2: reloadKey }));
centered.createChild('p').appendChild(i18n.i18n.getFormatLocalizedString(str_, UIStrings.afterRecordingSelectAnAreaOf, { PH1: navigateNode, PH2: learnMoreNode }));
if (isNode) {
const previewSection = new PanelFeedback.PanelFeedback.PanelFeedback();
previewSection.data = {
feedbackUrl: 'https://bugs.chromium.org/p/chromium/issues/detail?id=1354548',
quickStartUrl: 'https://developer.chrome.com/blog/js-profiler-deprecation/',
quickStartLinkText: i18nString(UIStrings.learnmore),
};
centered.appendChild(previewSection);
const feedbackButton = new PanelFeedback.FeedbackButton.FeedbackButton();
feedbackButton.data = {
feedbackUrl: 'https://bugs.chromium.org/p/chromium/issues/detail?id=1354548',
};
centered.appendChild(feedbackButton);
}
this.landingPage.show(this.statusPaneContainer);
}
hideLandingPage() {
this.landingPage.detach();
// Hide pane settings in trace view to conserve UI space, but preserve underlying setting.
this.sh