diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /devtools/client/performance/performance-view.js | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/performance/performance-view.js')
-rw-r--r-- | devtools/client/performance/performance-view.js | 490 |
1 files changed, 490 insertions, 0 deletions
diff --git a/devtools/client/performance/performance-view.js b/devtools/client/performance/performance-view.js new file mode 100644 index 0000000000..98775327b6 --- /dev/null +++ b/devtools/client/performance/performance-view.js @@ -0,0 +1,490 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* globals $, $$, PerformanceController */ +"use strict"; + +const EventEmitter = require("devtools/shared/event-emitter"); + +const React = require("devtools/client/shared/vendor/react"); +const ReactDOM = require("devtools/client/shared/vendor/react-dom"); + +const RecordingControls = React.createFactory( + require("devtools/client/performance/components/RecordingControls") +); +const RecordingButton = React.createFactory( + require("devtools/client/performance/components/RecordingButton") +); + +const EVENTS = require("devtools/client/performance/events"); +const PerformanceUtils = require("devtools/client/performance/modules/utils"); +const { DetailsView } = require("devtools/client/performance/views/details"); +const { OverviewView } = require("devtools/client/performance/views/overview"); +const { + RecordingsView, +} = require("devtools/client/performance/views/recordings"); +const { ToolbarView } = require("devtools/client/performance/views/toolbar"); + +const { L10N } = require("devtools/client/performance/modules/global"); +/** + * Master view handler for the performance tool. + */ +var PerformanceView = { + _state: null, + + // Set to true if the front emits a "buffer-status" event, indicating + // that the server has support for determining buffer status. + _bufferStatusSupported: false, + + // Mapping of state to selectors for different properties and their values, + // from the main profiler view. Used in `PerformanceView.setState()` + states: { + unavailable: [ + { + sel: "#performance-view", + opt: "selectedPanel", + val: () => $("#unavailable-notice"), + }, + { + sel: "#performance-view-content", + opt: "hidden", + val: () => true, + }, + ], + empty: [ + { + sel: "#performance-view", + opt: "selectedPanel", + val: () => $("#empty-notice"), + }, + { + sel: "#performance-view-content", + opt: "hidden", + val: () => true, + }, + ], + recording: [ + { + sel: "#performance-view", + opt: "selectedPanel", + val: () => $("#performance-view-content"), + }, + { + sel: "#performance-view-content", + opt: "hidden", + val: () => false, + }, + { + sel: "#details-pane-container", + opt: "selectedPanel", + val: () => $("#recording-notice"), + }, + ], + "console-recording": [ + { + sel: "#performance-view", + opt: "selectedPanel", + val: () => $("#performance-view-content"), + }, + { + sel: "#performance-view-content", + opt: "hidden", + val: () => false, + }, + { + sel: "#details-pane-container", + opt: "selectedPanel", + val: () => $("#console-recording-notice"), + }, + ], + recorded: [ + { + sel: "#performance-view", + opt: "selectedPanel", + val: () => $("#performance-view-content"), + }, + { + sel: "#performance-view-content", + opt: "hidden", + val: () => false, + }, + { + sel: "#details-pane-container", + opt: "selectedPanel", + val: () => $("#details-pane"), + }, + ], + loading: [ + { + sel: "#performance-view", + opt: "selectedPanel", + val: () => $("#performance-view-content"), + }, + { + sel: "#performance-view-content", + opt: "hidden", + val: () => false, + }, + { + sel: "#details-pane-container", + opt: "selectedPanel", + val: () => $("#loading-notice"), + }, + ], + }, + + /** + * Sets up the view with event binding and main subviews. + */ + async initialize() { + this._onRecordButtonClick = this._onRecordButtonClick.bind(this); + this._onImportButtonClick = this._onImportButtonClick.bind(this); + this._onClearButtonClick = this._onClearButtonClick.bind(this); + this._onRecordingSelected = this._onRecordingSelected.bind(this); + this._onProfilerStatusUpdated = this._onProfilerStatusUpdated.bind(this); + this._onRecordingStateChange = this._onRecordingStateChange.bind(this); + this._onNewRecordingFailed = this._onNewRecordingFailed.bind(this); + + // Bind to controller events to unlock the record button + PerformanceController.on( + EVENTS.RECORDING_SELECTED, + this._onRecordingSelected + ); + PerformanceController.on( + EVENTS.RECORDING_PROFILER_STATUS_UPDATE, + this._onProfilerStatusUpdated + ); + PerformanceController.on( + EVENTS.RECORDING_STATE_CHANGE, + this._onRecordingStateChange + ); + PerformanceController.on( + EVENTS.RECORDING_ADDED, + this._onRecordingStateChange + ); + PerformanceController.on( + EVENTS.BACKEND_FAILED_AFTER_RECORDING_START, + this._onNewRecordingFailed + ); + + if (await PerformanceController.canCurrentlyRecord()) { + this.setState("empty"); + } else { + this.setState("unavailable"); + } + + // Initialize the ToolbarView first, because other views may need access + // to the OptionsView via the controller, to read prefs. + await ToolbarView.initialize(); + await RecordingsView.initialize(); + await OverviewView.initialize(); + await DetailsView.initialize(); + + // DE-XUL: Begin migrating the toolbar to React. Temporarily hold state here. + this._recordingControlsState = { + onRecordButtonClick: this._onRecordButtonClick, + onImportButtonClick: this._onImportButtonClick, + onClearButtonClick: this._onClearButtonClick, + isRecording: false, + isDisabled: false, + }; + // Mount to an HTML element. + const { createHtmlMount } = PerformanceUtils; + this._recordingControlsMount = createHtmlMount( + $("#recording-controls-mount") + ); + this._recordingButtonsMounts = Array.from( + $$(".recording-button-mount") + ).map(createHtmlMount); + + this._renderRecordingControls(); + }, + + /** + * DE-XUL: Render the recording controls and buttons using React. + */ + _renderRecordingControls: function() { + ReactDOM.render( + RecordingControls(this._recordingControlsState), + this._recordingControlsMount + ); + for (const button of this._recordingButtonsMounts) { + ReactDOM.render(RecordingButton(this._recordingControlsState), button); + } + }, + + /** + * Unbinds events and destroys subviews. + */ + async destroy() { + PerformanceController.off( + EVENTS.RECORDING_SELECTED, + this._onRecordingSelected + ); + PerformanceController.off( + EVENTS.RECORDING_PROFILER_STATUS_UPDATE, + this._onProfilerStatusUpdated + ); + PerformanceController.off( + EVENTS.RECORDING_STATE_CHANGE, + this._onRecordingStateChange + ); + PerformanceController.off( + EVENTS.RECORDING_ADDED, + this._onRecordingStateChange + ); + PerformanceController.off( + EVENTS.BACKEND_FAILED_AFTER_RECORDING_START, + this._onNewRecordingFailed + ); + + await ToolbarView.destroy(); + await RecordingsView.destroy(); + await OverviewView.destroy(); + await DetailsView.destroy(); + }, + + /** + * Sets the state of the profiler view. Possible options are "unavailable", + * "empty", "recording", "console-recording", "recorded". + */ + setState: function(state) { + // Make sure that the focus isn't captured on a hidden iframe. This fixes a + // XUL bug where shortcuts stop working. + const iframes = window.document.querySelectorAll("iframe"); + for (const iframe of iframes) { + iframe.blur(); + } + window.focus(); + + const viewConfig = this.states[state]; + if (!viewConfig) { + throw new Error(`Invalid state for PerformanceView: ${state}`); + } + for (const { sel, opt, val } of viewConfig) { + for (const el of $$(sel)) { + el[opt] = val(); + } + } + + this._state = state; + + if (state === "console-recording") { + const recording = PerformanceController.getCurrentRecording(); + let label = recording.getLabel() || ""; + + // Wrap the label in quotes if it exists for the commands. + label = label ? `"${label}"` : ""; + + const startCommand = $( + ".console-profile-recording-notice .console-profile-command" + ); + const stopCommand = $( + ".console-profile-stop-notice .console-profile-command" + ); + + startCommand.value = `console.profile(${label})`; + stopCommand.value = `console.profileEnd(${label})`; + } + + this.updateBufferStatus(); + this.emit(EVENTS.UI_STATE_CHANGED, state); + }, + + /** + * Returns the state of the PerformanceView. + */ + getState: function() { + return this._state; + }, + + /** + * Reset the displayed buffer status. + * Called for every target-switching. + */ + resetBufferStatus() { + this._bufferStatusSupported = false; + $("#details-pane-container").removeAttribute("buffer-status"); + }, + + /** + * Updates the displayed buffer status. + */ + updateBufferStatus: function() { + // If we've never seen a "buffer-status" event from the front, ignore + // and keep the buffer elements hidden. + if (!this._bufferStatusSupported) { + return; + } + + const recording = PerformanceController.getCurrentRecording(); + if (!recording || !recording.isRecording()) { + return; + } + + const bufferUsage = + PerformanceController.getBufferUsageForRecording(recording) || 0; + + // Normalize to a percentage value + const percent = Math.floor(bufferUsage * 100); + + const $container = $("#details-pane-container"); + const $bufferLabel = $(".buffer-status-message", $container.selectedPanel); + + // Be a little flexible on the buffer status, although not sure how + // this could happen, as RecordingModel clamps. + if (percent >= 99) { + $container.setAttribute("buffer-status", "full"); + } else { + $container.setAttribute("buffer-status", "in-progress"); + } + + $bufferLabel.value = L10N.getFormatStr("profiler.bufferFull", percent); + this.emit(EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED, percent); + }, + + /** + * Toggles the `locked` attribute on the record buttons based + * on `lock`. + * + * @param {boolean} lock + */ + _lockRecordButtons: function(lock) { + this._recordingControlsState.isLocked = lock; + this._renderRecordingControls(); + }, + + /* + * Toggles the `checked` attribute on the record buttons based + * on `activate`. + * + * @param {boolean} activate + */ + _toggleRecordButtons: function(activate) { + this._recordingControlsState.isRecording = activate; + this._renderRecordingControls(); + }, + + /** + * When a recording has started. + */ + _onRecordingStateChange: function() { + const currentRecording = PerformanceController.getCurrentRecording(); + const recordings = PerformanceController.getRecordings(); + + this._toggleRecordButtons( + !!recordings.find(r => !r.isConsole() && r.isRecording()) + ); + this._lockRecordButtons( + !!recordings.find(r => !r.isConsole() && r.isFinalizing()) + ); + + if (currentRecording && currentRecording.isFinalizing()) { + this.setState("loading"); + } + if (currentRecording && currentRecording.isCompleted()) { + this.setState("recorded"); + } + if (currentRecording && currentRecording.isRecording()) { + this.updateBufferStatus(); + } + }, + + /** + * When starting a recording has failed. + */ + _onNewRecordingFailed: function() { + this._lockRecordButtons(false); + this._toggleRecordButtons(false); + }, + + /** + * Handler for clicking the clear button. + */ + _onClearButtonClick: function(e) { + this.emit(EVENTS.UI_CLEAR_RECORDINGS); + }, + + /** + * Handler for clicking the record button. + */ + _onRecordButtonClick: function(e) { + if (this._recordingControlsState.isRecording) { + this.emit(EVENTS.UI_STOP_RECORDING); + } else { + this._lockRecordButtons(true); + this._toggleRecordButtons(true); + this.emit(EVENTS.UI_START_RECORDING); + } + }, + + /** + * Handler for clicking the import button. + */ + _onImportButtonClick: function(e) { + const fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + fp.init( + window, + L10N.getStr("recordingsList.importDialogTitle"), + Ci.nsIFilePicker.modeOpen + ); + fp.appendFilter( + L10N.getStr("recordingsList.saveDialogJSONFilter"), + "*.json" + ); + fp.appendFilter(L10N.getStr("recordingsList.saveDialogAllFilter"), "*.*"); + + fp.open(rv => { + if (rv == Ci.nsIFilePicker.returnOK) { + this.emit(EVENTS.UI_IMPORT_RECORDING, fp.file); + } + }); + }, + + /** + * Fired when a recording is selected. Used to toggle the profiler view state. + */ + _onRecordingSelected: function(recording) { + if (!recording) { + this.setState("empty"); + } else if (recording.isRecording() && recording.isConsole()) { + this.setState("console-recording"); + } else if (recording.isRecording()) { + this.setState("recording"); + } else { + this.setState("recorded"); + } + }, + + /** + * Fired when the controller has updated information on the buffer's status. + * Update the buffer status display if shown. + */ + _onProfilerStatusUpdated: function(profilerStatus) { + // We only care about buffer status here, so check to see + // if it has position. + if (!profilerStatus || profilerStatus.position === void 0) { + return; + } + // If this is our first buffer event, set the status and add a class + if (!this._bufferStatusSupported) { + this._bufferStatusSupported = true; + $("#details-pane-container").setAttribute("buffer-status", "in-progress"); + } + + if (!this.getState("recording") && !this.getState("console-recording")) { + return; + } + + this.updateBufferStatus(); + }, + + toString: () => "[object PerformanceView]", +}; + +/** + * Convenient way of emitting events from the view. + */ +EventEmitter.decorate(PerformanceView); + +exports.PerformanceView = PerformanceView; |