From 2aa4a82499d4becd2284cdb482213d541b8804dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 28 Apr 2024 16:29:10 +0200 Subject: Adding upstream version 86.0.1. Signed-off-by: Daniel Baumann --- .../performance-new/components/AboutProfiling.js | 161 ++++++ .../performance-new/components/Description.js | 69 +++ .../performance-new/components/DevToolsPanel.js | 83 +++ .../components/DevToolsPresetSelection.js | 204 +++++++ .../performance-new/components/DirectoryPicker.js | 122 ++++ .../components/OnboardingMessage.js | 144 +++++ .../client/performance-new/components/Presets.js | 163 ++++++ .../components/ProfilerEventHandling.js | 269 +++++++++ .../client/performance-new/components/README.md | 3 + .../client/performance-new/components/Range.js | 79 +++ .../performance-new/components/RecordingButton.js | 254 +++++++++ .../client/performance-new/components/Settings.js | 620 +++++++++++++++++++++ .../client/performance-new/components/moz.build | 18 + 13 files changed, 2189 insertions(+) create mode 100644 devtools/client/performance-new/components/AboutProfiling.js create mode 100644 devtools/client/performance-new/components/Description.js create mode 100644 devtools/client/performance-new/components/DevToolsPanel.js create mode 100644 devtools/client/performance-new/components/DevToolsPresetSelection.js create mode 100644 devtools/client/performance-new/components/DirectoryPicker.js create mode 100644 devtools/client/performance-new/components/OnboardingMessage.js create mode 100644 devtools/client/performance-new/components/Presets.js create mode 100644 devtools/client/performance-new/components/ProfilerEventHandling.js create mode 100644 devtools/client/performance-new/components/README.md create mode 100644 devtools/client/performance-new/components/Range.js create mode 100644 devtools/client/performance-new/components/RecordingButton.js create mode 100644 devtools/client/performance-new/components/Settings.js create mode 100644 devtools/client/performance-new/components/moz.build (limited to 'devtools/client/performance-new/components') diff --git a/devtools/client/performance-new/components/AboutProfiling.js b/devtools/client/performance-new/components/AboutProfiling.js new file mode 100644 index 0000000000..3e8083a335 --- /dev/null +++ b/devtools/client/performance-new/components/AboutProfiling.js @@ -0,0 +1,161 @@ +/* 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/. */ +// @ts-check + +/** + * @template P + * @typedef {import("react-redux").ResolveThunks

} ResolveThunks

+ */ + +/** + * @typedef {Object} StateProps + * @property {boolean?} isSupportedPlatform + * @property {PageContext} pageContext + * @property {string | null} promptEnvRestart + * @property {(() => void) | undefined} openRemoteDevTools + */ + +/** + * @typedef {StateProps} Props + * @typedef {import("../@types/perf").State} StoreState + * @typedef {import("../@types/perf").PanelWindow} PanelWindow + *@typedef {import("../@types/perf").PageContext} PageContext + */ + +"use strict"; + +const { + PureComponent, + createFactory, +} = require("devtools/client/shared/vendor/react"); +const { connect } = require("devtools/client/shared/vendor/react-redux"); +const { + div, + h1, + button, +} = require("devtools/client/shared/vendor/react-dom-factories"); +const Localized = createFactory( + require("devtools/client/shared/vendor/fluent-react").Localized +); +const Settings = createFactory( + require("devtools/client/performance-new/components/Settings.js") +); +const Presets = createFactory( + require("devtools/client/performance-new/components/Presets") +); + +const selectors = require("devtools/client/performance-new/store/selectors"); +const { + restartBrowserWithEnvironmentVariable, +} = require("devtools/client/performance-new/browser"); + +/** + * This is the top level component for the about:profiling page. It shares components + * with the popup and DevTools page. + * + * @extends {React.PureComponent} + */ +class AboutProfiling extends PureComponent { + /** @param {Props} props */ + constructor(props) { + super(props); + this.handleRestart = this.handleRestart.bind(this); + } + + handleRestart() { + const { promptEnvRestart } = this.props; + if (!promptEnvRestart) { + throw new Error( + "handleRestart() should only be called when promptEnvRestart exists." + ); + } + restartBrowserWithEnvironmentVariable(promptEnvRestart, "1"); + } + + render() { + const { + isSupportedPlatform, + pageContext, + promptEnvRestart, + openRemoteDevTools, + } = this.props; + + if (isSupportedPlatform === null) { + // We don't know yet if this is a supported platform, wait for a response. + return null; + } + + return div( + { className: `perf perf-${pageContext}` }, + promptEnvRestart + ? div( + { className: "perf-env-restart" }, + div( + { + className: + "perf-photon-message-bar perf-photon-message-bar-warning perf-env-restart-fixed", + }, + div({ className: "perf-photon-message-bar-warning-icon" }), + Localized({ id: "perftools-status-restart-required" }), + button( + { + className: "perf-photon-button perf-photon-button-micro", + type: "button", + onClick: this.handleRestart, + }, + Localized({ id: "perftools-button-restart" }) + ) + ) + ) + : null, + + openRemoteDevTools + ? div( + { className: "perf-back" }, + button( + { + className: "perf-back-button", + type: "button", + onClick: openRemoteDevTools, + }, + Localized({ id: "perftools-button-save-settings" }) + ) + ) + : null, + + div( + { className: "perf-intro" }, + h1( + { className: "perf-intro-title" }, + Localized({ id: "perftools-intro-title" }) + ), + div( + { className: "perf-intro-row" }, + div({}, div({ className: "perf-intro-icon" })), + Localized({ + className: "perf-intro-text", + id: "perftools-intro-description", + }) + ) + ), + Presets(), + Settings() + ); + } +} + +/** + * @param {StoreState} state + * @returns {StateProps} + */ +function mapStateToProps(state) { + return { + isSupportedPlatform: selectors.getIsSupportedPlatform(state), + pageContext: selectors.getPageContext(state), + promptEnvRestart: selectors.getPromptEnvRestart(state), + openRemoteDevTools: selectors.getOpenRemoteDevTools(state), + }; +} + +module.exports = connect(mapStateToProps)(AboutProfiling); diff --git a/devtools/client/performance-new/components/Description.js b/devtools/client/performance-new/components/Description.js new file mode 100644 index 0000000000..49688d23ce --- /dev/null +++ b/devtools/client/performance-new/components/Description.js @@ -0,0 +1,69 @@ +/* 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/. */ +// @ts-check + +/** + * @typedef {{}} Props - This is an empty object. + */ + +"use strict"; + +const { + PureComponent, + createFactory, +} = require("devtools/client/shared/vendor/react"); +const { + div, + button, + p, +} = require("devtools/client/shared/vendor/react-dom-factories"); +const Localized = createFactory( + require("devtools/client/shared/vendor/fluent-react").Localized +); + +/** + * This component provides a helpful description for what is going on in the component + * and provides some external links. + * @extends {React.PureComponent} + */ +class Description extends PureComponent { + /** + * @param {Props} props + */ + constructor(props) { + super(props); + this.handleLinkClick = this.handleLinkClick.bind(this); + } + + /** + * @param {React.MouseEvent} event + */ + handleLinkClick(event) { + const { openDocLink } = require("devtools/client/shared/link"); + + /** @type HTMLButtonElement */ + const target = /** @type {any} */ (event.target); + + openDocLink(target.value, {}); + } + + render() { + return div( + { className: "perf-description" }, + Localized( + { + id: "perftools-description-intro", + a: button({ + className: "perf-external-link", + onClick: this.handleLinkClick, + value: "https://profiler.firefox.com", + }), + }, + p({}) + ) + ); + } +} + +module.exports = Description; diff --git a/devtools/client/performance-new/components/DevToolsPanel.js b/devtools/client/performance-new/components/DevToolsPanel.js new file mode 100644 index 0000000000..7c36c10682 --- /dev/null +++ b/devtools/client/performance-new/components/DevToolsPanel.js @@ -0,0 +1,83 @@ +/* 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/. */ +// @ts-check + +/** + * @template P + * @typedef {import("react-redux").ResolveThunks

} ResolveThunks

+ */ + +/** + * @typedef {Object} StateProps + * @property {boolean?} isSupportedPlatform + */ + +/** + * @typedef {StateProps} Props + * @typedef {import("../@types/perf").State} StoreState + * @typedef {import("../@types/perf").PanelWindow} PanelWindow + */ + +"use strict"; + +const { + PureComponent, + createFactory, +} = require("devtools/client/shared/vendor/react"); +const { connect } = require("devtools/client/shared/vendor/react-redux"); +const { + div, + hr, +} = require("devtools/client/shared/vendor/react-dom-factories"); +const RecordingButton = createFactory( + require("devtools/client/performance-new/components/RecordingButton") +); +const Description = createFactory( + require("devtools/client/performance-new/components/Description") +); +const DevToolsPresetSelection = createFactory( + require("devtools/client/performance-new/components/DevToolsPresetSelection") +); +const OnboardingMessage = createFactory( + require("devtools/client/performance-new/components/OnboardingMessage") +); + +const selectors = require("devtools/client/performance-new/store/selectors"); + +/** + * This is the top level component for the DevTools panel. + * + * @extends {React.PureComponent} + */ +class DevToolsPanel extends PureComponent { + render() { + const { isSupportedPlatform } = this.props; + + if (isSupportedPlatform === null) { + // We don't know yet if this is a supported platform, wait for a response. + return null; + } + + return div( + { className: `perf perf-devtools` }, + OnboardingMessage(), + RecordingButton(), + Description(), + hr({ className: "perf-presets-hr" }), + DevToolsPresetSelection() + ); + } +} + +/** + * @param {StoreState} state + * @returns {StateProps} + */ +function mapStateToProps(state) { + return { + isSupportedPlatform: selectors.getIsSupportedPlatform(state), + }; +} + +module.exports = connect(mapStateToProps)(DevToolsPanel); diff --git a/devtools/client/performance-new/components/DevToolsPresetSelection.js b/devtools/client/performance-new/components/DevToolsPresetSelection.js new file mode 100644 index 0000000000..fccfd6b46a --- /dev/null +++ b/devtools/client/performance-new/components/DevToolsPresetSelection.js @@ -0,0 +1,204 @@ +/* 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/. */ +// @ts-check + +/** + * @template P + * @typedef {import("react-redux").ResolveThunks

} ResolveThunks

+ */ + +/** + * @typedef {Object} StateProps + * @property {string} presetName + * @property {number} interval + * @property {string[]} threads + * @property {string[]} features + * @property {() => void} openAboutProfiling + * @property {import("../@types/perf").Presets} presets + */ + +/** + * @typedef {Object} ThunkDispatchProps + * @property {typeof actions.changePreset} changePreset + */ + +/** + * @typedef {ResolveThunks} DispatchProps + * @typedef {StateProps & DispatchProps} Props + * @typedef {import("../@types/perf").State} StoreState + * @typedef {import("../@types/perf").FeatureDescription} FeatureDescription + */ + +"use strict"; + +const { + PureComponent, + createFactory, +} = require("devtools/client/shared/vendor/react"); +const { + div, + select, + option, + button, + ul, + li, + span, +} = require("devtools/client/shared/vendor/react-dom-factories"); +const { connect } = require("devtools/client/shared/vendor/react-redux"); +const actions = require("devtools/client/performance-new/store/actions"); +const selectors = require("devtools/client/performance-new/store/selectors"); +const { + featureDescriptions, +} = require("devtools/client/performance-new/utils"); +const Localized = createFactory( + require("devtools/client/shared/vendor/fluent-react").Localized +); + +/** + * This component displays the preset selection for the DevTools panel. It should be + * basically the same implementation as the popup, but done in React. The popup + * is written using vanilla JS and browser chrome elements in order to be more + * performant. + * + * @extends {React.PureComponent} + */ +class DevToolsPresetSelection extends PureComponent { + /** @param {Props} props */ + constructor(props) { + super(props); + this.onPresetChange = this.onPresetChange.bind(this); + + /** + * Create an object map to easily look up feature description. + * @type {{[key: string]: FeatureDescription}} + */ + this.featureDescriptionMap = {}; + for (const feature of featureDescriptions) { + this.featureDescriptionMap[feature.value] = feature; + } + } + + /** + * Handle the select change. + * @param {React.ChangeEvent} event + */ + onPresetChange(event) { + const { presets } = this.props; + this.props.changePreset(presets, event.target.value); + } + + render() { + const { presetName, presets, openAboutProfiling } = this.props; + + let presetDescription; + const currentPreset = presets[presetName]; + if (currentPreset) { + // Display the current preset's description. + presetDescription = currentPreset.description; + } else { + // Build up a display of the details of the custom preset. + const { interval, threads, features } = this.props; + presetDescription = div( + null, + ul( + { className: "perf-presets-custom" }, + li( + null, + Localized( + { id: "perftools-devtools-interval-label" }, + span({ className: "perftools-presets-custom-bold" }) + ), + " ", + Localized({ + id: "perftools-range-interval-milliseconds", + $interval: interval, + }) + ), + li( + null, + Localized( + { id: "perftools-devtools-threads-label" }, + span({ className: "perf-presets-custom-bold" }) + ), + " ", + threads.join(", ") + ), + features.map(feature => { + const description = this.featureDescriptionMap[feature]; + if (!description) { + throw new Error( + "Could not find the feature description for " + feature + ); + } + return li( + { key: feature }, + description ? description.name : feature + ); + }) + ), + button( + { className: "perf-external-link", onClick: openAboutProfiling }, + Localized({ id: "perftools-button-edit-settings" }) + ) + ); + } + + return div( + { className: "perf-presets" }, + div( + { className: "perf-presets-settings" }, + Localized({ id: "perftools-devtools-settings-label" }) + ), + div( + { className: "perf-presets-details" }, + div( + { className: "perf-presets-details-row" }, + select( + { + className: "perf-presets-select", + onChange: this.onPresetChange, + value: presetName, + }, + Object.entries(presets).map(([name, preset]) => + option({ key: name, value: name }, preset.label) + ), + option({ value: "custom" }, "Custom") + ) + // The overhead component will go here. + ), + div( + { className: "perf-presets-details-row perf-presets-description" }, + presetDescription + ) + ) + ); + } +} + +/** + * @param {StoreState} state + * @returns {StateProps} + */ +function mapStateToProps(state) { + return { + presetName: selectors.getPresetName(state), + presets: selectors.getPresets(state), + interval: selectors.getInterval(state), + threads: selectors.getThreads(state), + features: selectors.getFeatures(state), + openAboutProfiling: selectors.getOpenAboutProfiling(state), + }; +} + +/** + * @type {ThunkDispatchProps} + */ +const mapDispatchToProps = { + changePreset: actions.changePreset, +}; + +module.exports = connect( + mapStateToProps, + mapDispatchToProps +)(DevToolsPresetSelection); diff --git a/devtools/client/performance-new/components/DirectoryPicker.js b/devtools/client/performance-new/components/DirectoryPicker.js new file mode 100644 index 0000000000..1bd667353e --- /dev/null +++ b/devtools/client/performance-new/components/DirectoryPicker.js @@ -0,0 +1,122 @@ +/* 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/. */ +// @ts-check + +/** + * @typedef {Object} Props + * @property {string[]} dirs + * @property {() => void} onAdd + * @property {(index: number) => void} onRemove + */ + +"use strict"; + +const { + PureComponent, + createFactory, +} = require("devtools/client/shared/vendor/react"); +const { + div, + button, + select, + option, +} = require("devtools/client/shared/vendor/react-dom-factories"); +const { + withCommonPathPrefixRemoved, +} = require("devtools/client/performance-new/utils"); +const Localized = createFactory( + require("devtools/client/shared/vendor/fluent-react").Localized +); + +/** + * A list of directories with add and remove buttons. + * Looks like this: + * + * +---------------------------------------------+ + * | code/obj-m-android-opt | + * | code/obj-m-android-debug | + * | test/obj-m-test | + * | | + * +---------------------------------------------+ + * + * [+] [-] + * + * @extends {React.PureComponent} + */ +class DirectoryPicker extends PureComponent { + /** @param {Props} props */ + constructor(props) { + super(props); + this._listBox = null; + this._takeListBoxRef = this._takeListBoxRef.bind(this); + this._handleAddButtonClick = this._handleAddButtonClick.bind(this); + this._handleRemoveButtonClick = this._handleRemoveButtonClick.bind(this); + } + + /** + * @param {HTMLSelectElement} element + */ + _takeListBoxRef(element) { + this._listBox = element; + } + + _handleAddButtonClick() { + this.props.onAdd(); + } + + _handleRemoveButtonClick() { + if (this._listBox && this._listBox.selectedIndex !== -1) { + this.props.onRemove(this._listBox.selectedIndex); + } + } + + render() { + const { dirs } = this.props; + const truncatedDirs = withCommonPathPrefixRemoved(dirs); + return [ + select( + { + className: "perf-settings-dir-list", + size: 4, + ref: this._takeListBoxRef, + key: "directory-picker-select", + }, + dirs.map((fullPath, i) => + option( + { + key: fullPath, + className: "pref-settings-dir-list-item", + title: fullPath, + }, + truncatedDirs[i] + ) + ) + ), + div( + { + className: "perf-settings-dir-list-button-group", + key: "directory-picker-div", + }, + button( + { + type: "button", + className: `perf-photon-button perf-photon-button-default perf-button`, + onClick: this._handleAddButtonClick, + }, + Localized({ id: "perftools-button-add-directory" }) + ), + button( + { + type: "button", + className: `perf-photon-button perf-photon-button-default perf-button`, + onClick: this._handleRemoveButtonClick, + }, + Localized({ id: "perftools-button-remove-directory" }) + ) + ), + ]; + } +} + +module.exports = DirectoryPicker; diff --git a/devtools/client/performance-new/components/OnboardingMessage.js b/devtools/client/performance-new/components/OnboardingMessage.js new file mode 100644 index 0000000000..740d1e6ff4 --- /dev/null +++ b/devtools/client/performance-new/components/OnboardingMessage.js @@ -0,0 +1,144 @@ +/* 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/. */ +// @ts-check + +/** + * @typedef {{}} Props - This is an empty object. + */ + +/** + * @typedef {Object} State + * @property {boolean} isOnboardingEnabled + */ + +/** + * @typedef {import("../@types/perf").PanelWindow} PanelWindow + */ + +"use strict"; + +const { PureComponent } = require("devtools/client/shared/vendor/react"); +const { + b, + button, + div, + p, +} = require("devtools/client/shared/vendor/react-dom-factories"); + +const Services = require("Services"); +const { openDocLink } = require("devtools/client/shared/link"); + +const LEARN_MORE_URL = + "https://developer.mozilla.org/docs/Mozilla/Performance/Profiling_with_the_Built-in_Profiler"; +const ONBOARDING_PREF = "devtools.performance.new-panel-onboarding"; + +/** + * This component provides a temporary onboarding message for users migrating + * from the old DevTools performance panel. + * @extends {React.PureComponent} + */ +class OnboardingMessage extends PureComponent { + /** + * @param {Props} props + */ + constructor(props) { + super(props); + + // The preference has no default value for new profiles. + // If it is missing, default to true to show the message by default. + const isOnboardingEnabled = Services.prefs.getBoolPref( + ONBOARDING_PREF, + true + ); + + /** @type {State} */ + this.state = { isOnboardingEnabled }; + } + + componentDidMount() { + Services.prefs.addObserver(ONBOARDING_PREF, this.onPreferenceUpdated); + } + + componentWillUnmount() { + Services.prefs.removeObserver(ONBOARDING_PREF, this.onPreferenceUpdated); + } + + handleCloseIconClick = () => { + Services.prefs.setBoolPref(ONBOARDING_PREF, false); + }; + + handleLearnMoreClick = () => { + openDocLink(LEARN_MORE_URL, {}); + }; + + handleSettingsClick = () => { + /** @type {any} */ + const anyWindow = window; + /** @type {PanelWindow} - Coerce the window into the PanelWindow. */ + const { gToolbox } = anyWindow; + gToolbox.selectTool("options"); + }; + + /** + * Update the state whenever the devtools.performance.new-panel-onboarding + * preference is updated. + */ + onPreferenceUpdated = () => { + const value = Services.prefs.getBoolPref(ONBOARDING_PREF, true); + this.setState({ isOnboardingEnabled: value }); + }; + + render() { + const { isOnboardingEnabled } = this.state; + if (!isOnboardingEnabled) { + return null; + } + + const learnMoreLink = button( + { + className: "perf-external-link", + onClick: this.handleLearnMoreClick, + }, + "Learn more" + ); + + const settingsLink = button( + { + className: "perf-external-link", + onClick: this.handleSettingsClick, + }, + "Settings > Advanced" + ); + + const closeButton = button({ + "aria-label": "Close the onboarding message", + className: + "perf-onboarding-close-button perf-photon-button perf-photon-button-ghost", + onClick: this.handleCloseIconClick, + }); + + return div( + { className: "perf-onboarding" }, + div( + { className: "perf-onboarding-message" }, + p( + { className: "perf-onboarding-message-row" }, + b({}, "New"), + ": Firefox Profiler is now integrated into Developer Tools. ", + learnMoreLink, + " about this powerful new tool." + ), + p( + { className: "perf-onboarding-message-row" }, + "(For a limited time, you can access the original Performance panel via ", + settingsLink, + ")" + ) + ), + closeButton + ); + } +} + +module.exports = OnboardingMessage; diff --git a/devtools/client/performance-new/components/Presets.js b/devtools/client/performance-new/components/Presets.js new file mode 100644 index 0000000000..4bb6533a49 --- /dev/null +++ b/devtools/client/performance-new/components/Presets.js @@ -0,0 +1,163 @@ +/* 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/. */ +// @ts-check + +/** + * @template P + * @typedef {import("react-redux").ResolveThunks

} ResolveThunks

+ */ + +"use strict"; +const { + PureComponent, + createElement, +} = require("devtools/client/shared/vendor/react"); +const { + div, + label, + input, +} = require("devtools/client/shared/vendor/react-dom-factories"); +const selectors = require("devtools/client/performance-new/store/selectors"); +const actions = require("devtools/client/performance-new/store/actions"); +const { connect } = require("devtools/client/shared/vendor/react-redux"); + +/** + * @typedef {Object} PresetProps + * @property {string} presetName + * @property {boolean} selected + * @property {import("../@types/perf").PresetDefinition | null} preset + * @property {(presetName: string) => void} onChange + */ + +/** + * Switch between various profiler presets, which will override the individualized + * settings for the profiler. + * + * @extends {React.PureComponent} + */ +class Preset extends PureComponent { + /** + * Handle the checkbox change. + * @param {React.ChangeEvent} event + */ + onChange = event => { + this.props.onChange(event.target.value); + }; + + render() { + const { preset, presetName, selected } = this.props; + let labelText, description; + if (preset) { + labelText = preset.label; + description = preset.description; + } else { + labelText = "Custom"; + } + return label( + { className: "perf-presets-label" }, + div( + { className: "perf-presets-input-container" }, + input({ + className: "perf-presets-input", + type: "radio", + name: "presets", + value: presetName, + checked: selected, + onChange: this.onChange, + }) + ), + div( + { className: "perf-presets-text" }, + div({ className: "pref-preset-text-label" }, labelText), + description + ? div({ className: "perf-presets-description" }, description) + : null + ) + ); + } +} + +/** + * @typedef {Object} StateProps + * @property {string} selectedPresetName + * @property {import("../@types/perf").Presets} presets + */ + +/** + * @typedef {Object} ThunkDispatchProps + * @property {typeof actions.changePreset} changePreset + */ + +/** + * @typedef {ResolveThunks} DispatchProps + * @typedef {StateProps & DispatchProps} Props + * @typedef {import("../@types/perf").State} StoreState + */ + +/** + * Switch between various profiler presets, which will override the individualized + * settings for the profiler. + * + * @extends {React.PureComponent} + */ +class Presets extends PureComponent { + /** @param {Props} props */ + constructor(props) { + super(props); + this.onChange = this.onChange.bind(this); + } + + /** + * Handle the checkbox change. + * @param {string} presetName + */ + onChange(presetName) { + const { presets } = this.props; + this.props.changePreset(presets, presetName); + } + + render() { + const { presets, selectedPresetName } = this.props; + + return div( + { className: "perf-presets" }, + Object.entries(presets).map(([presetName, preset]) => + createElement(Preset, { + key: presetName, + presetName, + preset, + selected: presetName === selectedPresetName, + onChange: this.onChange, + }) + ), + createElement(Preset, { + key: "custom", + presetName: "custom", + selected: selectedPresetName == "custom", + preset: null, + onChange: this.onChange, + }) + ); + } +} + +/** + * @param {StoreState} state + * @returns {StateProps} + */ +function mapStateToProps(state) { + return { + selectedPresetName: selectors.getPresetName(state), + presets: selectors.getPresets(state), + }; +} + +/** + * @type {ThunkDispatchProps} + */ +const mapDispatchToProps = { + changePreset: actions.changePreset, +}; + +module.exports = connect(mapStateToProps, mapDispatchToProps)(Presets); diff --git a/devtools/client/performance-new/components/ProfilerEventHandling.js b/devtools/client/performance-new/components/ProfilerEventHandling.js new file mode 100644 index 0000000000..e3b07ff2bc --- /dev/null +++ b/devtools/client/performance-new/components/ProfilerEventHandling.js @@ -0,0 +1,269 @@ +/* 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/. */ +// @ts-check + +/** + * @template P + * @typedef {import("react-redux").ResolveThunks

} ResolveThunks

+ */ + +/** + * @typedef {Object} StateProps + * @property {PerfFront} perfFront + * @property {RecordingState} recordingState + * @property {boolean?} isSupportedPlatform + * @property {PageContext} pageContext + * @property {string | null} promptEnvRestart + */ + +/** + * @typedef {Object} ThunkDispatchProps + * @property {typeof actions.changeRecordingState} changeRecordingState + * @property {typeof actions.reportProfilerReady} reportProfilerReady + */ + +/** + * @typedef {ResolveThunks} DispatchProps + * @typedef {StateProps & DispatchProps} Props + * @typedef {import("../@types/perf").PerfFront} PerfFront + * @typedef {import("../@types/perf").RecordingState} RecordingState + * @typedef {import("../@types/perf").State} StoreState + * @typedef {import("../@types/perf").PageContext} PageContext + */ + +/** + * @typedef {import("../@types/perf").PanelWindow} PanelWindow + */ + +"use strict"; + +const { PureComponent } = require("devtools/client/shared/vendor/react"); +const { connect } = require("devtools/client/shared/vendor/react-redux"); +const actions = require("devtools/client/performance-new/store/actions"); +const selectors = require("devtools/client/performance-new/store/selectors"); + +/** + * This component state changes for the performance recording. e.g. If the profiler + * suddenly becomes unavailable, it needs to react to those changes, and update the + * recordingState in the store. + * + * @extends {React.PureComponent} + */ +class ProfilerEventHandling extends PureComponent { + /** @param {Props} props */ + constructor(props) { + super(props); + this.handleProfilerStarting = this.handleProfilerStarting.bind(this); + this.handleProfilerStopping = this.handleProfilerStopping.bind(this); + this.handlePrivateBrowsingStarting = this.handlePrivateBrowsingStarting.bind( + this + ); + this.handlePrivateBrowsingEnding = this.handlePrivateBrowsingEnding.bind( + this + ); + } + + componentDidMount() { + const { perfFront, reportProfilerReady } = this.props; + + // Ask for the initial state of the profiler. + Promise.all([ + perfFront.isActive(), + perfFront.isSupportedPlatform(), + perfFront.isLockedForPrivateBrowsing(), + ]).then(results => { + const [ + isActive, + isSupportedPlatform, + isLockedForPrivateBrowsing, + ] = results; + + let recordingState = this.props.recordingState; + // It's theoretically possible we got an event that already let us know about + // the current state of the profiler. + if (recordingState === "not-yet-known" && isSupportedPlatform) { + if (isLockedForPrivateBrowsing) { + recordingState = "locked-by-private-browsing"; + } else if (isActive) { + recordingState = "recording"; + } else { + recordingState = "available-to-record"; + } + } + reportProfilerReady(isSupportedPlatform, recordingState); + + // If this component is inside the popup, then report it being ready so that + // it will show. This defers the initial visibility of the popup until the + // React components have fully rendered, and thus there is no annoying "blip" + // to the screen when the page goes from fully blank, to showing the content. + /** @type {any} */ + const anyWindow = window; + /** @type {PanelWindow} - Coerce the window into the PanelWindow. */ + const { gReportReady } = anyWindow; + if (gReportReady) { + gReportReady(); + } + }); + + // Handle when the profiler changes state. It might be us, it might be someone else. + this.props.perfFront.on("profiler-started", this.handleProfilerStarting); + this.props.perfFront.on("profiler-stopped", this.handleProfilerStopping); + this.props.perfFront.on( + "profile-locked-by-private-browsing", + this.handlePrivateBrowsingStarting + ); + this.props.perfFront.on( + "profile-unlocked-from-private-browsing", + this.handlePrivateBrowsingEnding + ); + } + + componentWillUnmount() { + switch (this.props.recordingState) { + case "not-yet-known": + case "available-to-record": + case "request-to-stop-profiler": + case "request-to-get-profile-and-stop-profiler": + case "locked-by-private-browsing": + // Do nothing for these states. + break; + + case "recording": + case "request-to-start-recording": + this.props.perfFront.stopProfilerAndDiscardProfile(); + break; + + default: + throw new Error("Unhandled recording state."); + } + } + + handleProfilerStarting() { + const { changeRecordingState, recordingState } = this.props; + switch (recordingState) { + case "not-yet-known": + // We couldn't have started it yet, so it must have been someone + // else. (fallthrough) + case "available-to-record": + // We aren't recording, someone else started it up. (fallthrough) + case "request-to-stop-profiler": + // We requested to stop the profiler, but someone else already started + // it up. (fallthrough) + case "request-to-get-profile-and-stop-profiler": + changeRecordingState("recording"); + break; + + case "request-to-start-recording": + // Wait for the profiler to tell us that it has started. + changeRecordingState("recording"); + break; + + case "locked-by-private-browsing": + case "recording": + // These state cases don't make sense to happen, and means we have a logical + // fallacy somewhere. + throw new Error( + "The profiler started recording, when it shouldn't have " + + `been able to. Current state: "${recordingState}"` + ); + default: + throw new Error("Unhandled recording state"); + } + } + + handleProfilerStopping() { + const { changeRecordingState, recordingState } = this.props; + switch (recordingState) { + case "not-yet-known": + case "request-to-get-profile-and-stop-profiler": + case "request-to-stop-profiler": + changeRecordingState("available-to-record"); + break; + + case "request-to-start-recording": + // Highly unlikely, but someone stopped the recorder, this is fine. + // Do nothing (fallthrough). + case "locked-by-private-browsing": + // The profiler is already locked, so we know about this already. + break; + + case "recording": + changeRecordingState("available-to-record", { + didRecordingUnexpectedlyStopped: true, + }); + break; + + case "available-to-record": + throw new Error( + "The profiler stopped recording, when it shouldn't have been able to." + ); + default: + throw new Error("Unhandled recording state"); + } + } + + handlePrivateBrowsingStarting() { + const { recordingState, changeRecordingState } = this.props; + + switch (recordingState) { + case "request-to-get-profile-and-stop-profiler": + // This one is a tricky case. Go ahead and act like nothing went wrong, maybe + // it will resolve correctly? (fallthrough) + case "request-to-stop-profiler": + case "available-to-record": + case "not-yet-known": + changeRecordingState("locked-by-private-browsing"); + break; + + case "request-to-start-recording": + case "recording": + changeRecordingState("locked-by-private-browsing", { + didRecordingUnexpectedlyStopped: false, + }); + break; + + case "locked-by-private-browsing": + // Do nothing + break; + + default: + throw new Error("Unhandled recording state"); + } + } + + handlePrivateBrowsingEnding() { + // No matter the state, go ahead and set this as ready to record. This should + // be the only logical state to go into. + this.props.changeRecordingState("available-to-record"); + } + + render() { + return null; + } +} + +/** + * @param {StoreState} state + * @returns {StateProps} + */ +function mapStateToProps(state) { + return { + perfFront: selectors.getPerfFront(state), + recordingState: selectors.getRecordingState(state), + isSupportedPlatform: selectors.getIsSupportedPlatform(state), + pageContext: selectors.getPageContext(state), + promptEnvRestart: selectors.getPromptEnvRestart(state), + }; +} + +/** @type {ThunkDispatchProps} */ +const mapDispatchToProps = { + changeRecordingState: actions.changeRecordingState, + reportProfilerReady: actions.reportProfilerReady, +}; + +module.exports = connect( + mapStateToProps, + mapDispatchToProps +)(ProfilerEventHandling); diff --git a/devtools/client/performance-new/components/README.md b/devtools/client/performance-new/components/README.md new file mode 100644 index 0000000000..ee33630140 --- /dev/null +++ b/devtools/client/performance-new/components/README.md @@ -0,0 +1,3 @@ +# Performance New Components + +This folder contains the components that are used for about:profiling, the devtools panel, and the about:debugging remote profiling view. The components are NOT used for the popup, which does not use React / Redux. diff --git a/devtools/client/performance-new/components/Range.js b/devtools/client/performance-new/components/Range.js new file mode 100644 index 0000000000..026b256eb1 --- /dev/null +++ b/devtools/client/performance-new/components/Range.js @@ -0,0 +1,79 @@ +/* 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/. */ +// @ts-check + +/** + * @typedef {import("../@types/perf").ScaleFunctions} ScaleFunctions + */ + +/** + * @typedef {Object} Props + * @property {number} value + * @property {React.ReactNode} label + * @property {string} id + * @property {ScaleFunctions} scale + * @property {(value: number) => unknown} onChange + * @property {(value: number) => React.ReactNode} display + */ +"use strict"; +const { PureComponent } = require("devtools/client/shared/vendor/react"); +const { + div, + input, + label, +} = require("devtools/client/shared/vendor/react-dom-factories"); + +/** + * Provide a numeric range slider UI that works off of custom numeric scales. + * @extends React.PureComponent + */ +class Range extends PureComponent { + /** @param {Props} props */ + constructor(props) { + super(props); + this.handleInput = this.handleInput.bind(this); + } + + /** + * @param {React.ChangeEvent} event + */ + handleInput(event) { + event.preventDefault(); + const { scale, onChange } = this.props; + const frac = Number(event.target.value) / 100; + onChange(scale.fromFractionToSingleDigitValue(frac)); + } + + render() { + const { label: labelText, scale, id, value, display } = this.props; + return div( + { className: "perf-settings-row" }, + label( + { + className: "perf-settings-label", + htmlFor: id, + }, + labelText + ), + div( + { className: "perf-settings-value" }, + div( + { className: "perf-settings-range-input" }, + input({ + type: "range", + className: `perf-settings-range-input-el`, + min: "0", + max: "100", + value: scale.fromValueToFraction(value) * 100, + onChange: this.handleInput, + id, + }) + ), + div({ className: `perf-settings-range-value` }, display(value)) + ) + ); + } +} + +module.exports = Range; diff --git a/devtools/client/performance-new/components/RecordingButton.js b/devtools/client/performance-new/components/RecordingButton.js new file mode 100644 index 0000000000..e6dccc329c --- /dev/null +++ b/devtools/client/performance-new/components/RecordingButton.js @@ -0,0 +1,254 @@ +/* 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/. */ +// @ts-check + +/** + * @template P + * @typedef {import("react-redux").ResolveThunks

} ResolveThunks

+ */ + +/** + * @typedef {Object} StateProps + * @property {RecordingState} recordingState + * @property {boolean} isSupportedPlatform + * @property {boolean} recordingUnexpectedlyStopped + * @property {PageContext} pageContext + */ + +/** + * @typedef {Object} ThunkDispatchProps + * @property {typeof actions.startRecording} startRecording + * @property {typeof actions.getProfileAndStopProfiler} getProfileAndStopProfiler + * @property {typeof actions.stopProfilerAndDiscardProfile} stopProfilerAndDiscardProfile + + */ + +/** + * @typedef {ResolveThunks} DispatchProps + * @typedef {StateProps & DispatchProps} Props + * @typedef {import("../@types/perf").RecordingState} RecordingState + * @typedef {import("../@types/perf").State} StoreState + * @typedef {import("../@types/perf").PageContext} PageContext + */ + +"use strict"; + +const { PureComponent } = require("devtools/client/shared/vendor/react"); +const { + div, + button, + span, + img, +} = require("devtools/client/shared/vendor/react-dom-factories"); +const { connect } = require("devtools/client/shared/vendor/react-redux"); +const actions = require("devtools/client/performance-new/store/actions"); +const selectors = require("devtools/client/performance-new/store/selectors"); +const React = require("devtools/client/shared/vendor/react"); +const Localized = React.createFactory( + require("devtools/client/shared/vendor/fluent-react").Localized +); + +/** + * This component is not responsible for the full life cycle of recording a profile. It + * is only responsible for the actual act of stopping and starting recordings. It + * also reacts to the changes of the recording state from external changes. + * + * @extends {React.PureComponent} + */ +class RecordingButton extends PureComponent { + render() { + const { + startRecording, + stopProfilerAndDiscardProfile, + recordingState, + isSupportedPlatform, + recordingUnexpectedlyStopped, + getProfileAndStopProfiler, + } = this.props; + + if (!isSupportedPlatform) { + return renderButton({ + label: startRecordingLabel(), + isPrimary: true, + disabled: true, + additionalMessage: + // No need to localize as this string is not displayed to Tier-1 platforms. + "Your platform is not supported. The Gecko Profiler only " + + "supports Tier-1 platforms.", + }); + } + + switch (recordingState) { + case "not-yet-known": + return null; + + case "available-to-record": + return renderButton({ + onClick: startRecording, + isPrimary: true, + label: startRecordingLabel(), + additionalMessage: recordingUnexpectedlyStopped + ? Localized( + { id: "perftools-status-recording-stopped-by-another-tool" }, + div(null, "The recording was stopped by another tool.") + ) + : null, + }); + + case "request-to-stop-profiler": + return renderButton({ + label: Localized( + { id: "perftools-request-to-stop-profiler" }, + "Stopping recording" + ), + disabled: true, + }); + + case "request-to-get-profile-and-stop-profiler": + return renderButton({ + label: Localized( + { id: "perftools-request-to-get-profile-and-stop-profiler" }, + "Capturing profile" + ), + disabled: true, + }); + + case "request-to-start-recording": + case "recording": + return renderButton({ + label: span( + null, + Localized( + { id: "perftools-button-capture-recording" }, + "Capture recording" + ), + img({ + className: "perf-button-image", + alt: "", + /* This icon is actually the "open in new page" icon. */ + src: "chrome://devtools/skin/images/dock-undock.svg", + }) + ), + isPrimary: true, + onClick: getProfileAndStopProfiler, + disabled: recordingState === "request-to-start-recording", + additionalButton: { + label: Localized( + { id: "perftools-button-cancel-recording" }, + "Cancel recording" + ), + onClick: stopProfilerAndDiscardProfile, + }, + }); + + case "locked-by-private-browsing": + return renderButton({ + label: startRecordingLabel(), + isPrimary: true, + disabled: true, + additionalMessage: Localized( + { id: "perftools-status-private-browsing-notice" }, + `The profiler is disabled when Private Browsing is enabled. + Close all Private Windows to re-enable the profiler` + ), + }); + + default: + throw new Error("Unhandled recording state"); + } + } +} + +/** + * @param {{ + * disabled?: boolean, + * label?: React.ReactNode, + * onClick?: any, + * additionalMessage?: React.ReactNode, + * isPrimary?: boolean, + * pageContext?: PageContext, + * additionalButton?: { + * label: React.ReactNode, + * onClick: any, + * }, + * }} buttonSettings + */ +function renderButton(buttonSettings) { + const { + disabled, + label, + onClick, + additionalMessage, + isPrimary, + // pageContext, + additionalButton, + } = buttonSettings; + + const buttonClass = isPrimary ? "primary" : "default"; + + return div( + { className: "perf-button-container" }, + div( + null, + button( + { + className: `perf-photon-button perf-photon-button-${buttonClass} perf-button`, + disabled, + onClick, + }, + label + ), + additionalButton + ? button( + { + className: `perf-photon-button perf-photon-button-default perf-button`, + onClick: additionalButton.onClick, + disabled, + }, + additionalButton.label + ) + : null + ), + additionalMessage + ? div({ className: "perf-additional-message" }, additionalMessage) + : null + ); +} + +function startRecordingLabel() { + return span( + null, + Localized({ id: "perftools-button-start-recording" }, "Start recording"), + img({ + className: "perf-button-image", + alt: "", + /* This icon is actually the "open in new page" icon. */ + src: "chrome://devtools/skin/images/dock-undock.svg", + }) + ); +} + +/** + * @param {StoreState} state + * @returns {StateProps} + */ +function mapStateToProps(state) { + return { + recordingState: selectors.getRecordingState(state), + isSupportedPlatform: selectors.getIsSupportedPlatform(state), + recordingUnexpectedlyStopped: selectors.getRecordingUnexpectedlyStopped( + state + ), + pageContext: selectors.getPageContext(state), + }; +} + +/** @type {ThunkDispatchProps} */ +const mapDispatchToProps = { + startRecording: actions.startRecording, + stopProfilerAndDiscardProfile: actions.stopProfilerAndDiscardProfile, + getProfileAndStopProfiler: actions.getProfileAndStopProfiler, +}; + +module.exports = connect(mapStateToProps, mapDispatchToProps)(RecordingButton); diff --git a/devtools/client/performance-new/components/Settings.js b/devtools/client/performance-new/components/Settings.js new file mode 100644 index 0000000000..96803136a3 --- /dev/null +++ b/devtools/client/performance-new/components/Settings.js @@ -0,0 +1,620 @@ +/* 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/. */ +// @ts-check + +/** + * @typedef {Object} StateProps + * @property {number} interval + * @property {number} entries + * @property {string[]} features + * @property {string[]} threads + * @property {string} threadsString + * @property {string[]} objdirs + * @property {string[]} supportedFeatures + */ + +/** + * @typedef {Object} ThunkDispatchProps + * @property {typeof actions.changeInterval} changeInterval + * @property {typeof actions.changeEntries} changeEntries + * @property {typeof actions.changeFeatures} changeFeatures + * @property {typeof actions.changeThreads} changeThreads + * @property {typeof actions.changeObjdirs} changeObjdirs + */ + +/** + * @typedef {ResolveThunks} DispatchProps + */ + +/** + * @typedef {Object} State + * @property {null | string} temporaryThreadText + */ + +/** + * @typedef {import("../@types/perf").PopupWindow} PopupWindow + * @typedef {import("../@types/perf").State} StoreState + * @typedef {import("../@types/perf").FeatureDescription} FeatureDescription + * + * @typedef {StateProps & DispatchProps} Props + */ + +/** + * @template P + * @typedef {import("react-redux").ResolveThunks

} ResolveThunks

+ */ + +/** + * @template InjectedProps + * @template NeededProps + * @typedef {import("react-redux") + * .InferableComponentEnhancerWithProps + * } InferableComponentEnhancerWithProps + */ +"use strict"; + +const { + PureComponent, + createFactory, +} = require("devtools/client/shared/vendor/react"); +const { + div, + label, + input, + h1, + h2, + h3, + section, + p, +} = require("devtools/client/shared/vendor/react-dom-factories"); +const Range = createFactory( + require("devtools/client/performance-new/components/Range") +); +const DirectoryPicker = createFactory( + require("devtools/client/performance-new/components/DirectoryPicker") +); +const { + makeExponentialScale, + makePowerOf2Scale, + formatFileSize, + featureDescriptions, +} = require("devtools/client/performance-new/utils"); +const { connect } = require("devtools/client/shared/vendor/react-redux"); +const actions = require("devtools/client/performance-new/store/actions"); +const selectors = require("devtools/client/performance-new/store/selectors"); +const { + openFilePickerForObjdir, +} = require("devtools/client/performance-new/browser"); +const Localized = createFactory( + require("devtools/client/shared/vendor/fluent-react").Localized +); + +// The Gecko Profiler interprets the "entries" setting as 8 bytes per entry. +const PROFILE_ENTRY_SIZE = 8; + +/** + * @typedef {{ name: string, id: string, l10nId: string }} ThreadColumn + */ + +/** @type {Array} */ +const threadColumns = [ + [ + { + name: "GeckoMain", + id: "gecko-main", + // The l10nId take the form `perf-thread-${id}`, but isn't done programmatically + // so that it is easy to search in the codebase. + l10nId: "perftools-thread-gecko-main", + }, + { + name: "Compositor", + id: "compositor", + l10nId: "perftools-thread-compositor", + }, + { + name: "DOM Worker", + id: "dom-worker", + l10nId: "perftools-thread-dom-worker", + }, + { + name: "Renderer", + id: "renderer", + l10nId: "perftools-thread-renderer", + }, + ], + [ + { + name: "RenderBackend", + id: "render-backend", + l10nId: "perftools-thread-render-backend", + }, + { + name: "PaintWorker", + id: "paint-worker", + l10nId: "perftools-thread-paint-worker", + }, + { + name: "StyleThread", + id: "style-thread", + l10nId: "perftools-thread-style-thread", + }, + { + name: "Socket Thread", + id: "socket-thread", + l10nId: "perftools-thread-socket-thread", + }, + ], + [ + { + name: "StreamTrans", + id: "stream-trans", + l10nId: "pref-thread-stream-trans", + }, + { + name: "ImgDecoder", + id: "img-decoder", + l10nId: "perftools-thread-dns-resolver", + }, + { + name: "DNS Resolver", + id: "dns-resolver", + l10nId: "perftools-thread-dns-resolver", + }, + { + name: "JS Helper", + id: "js-helper", + l10nId: "perftools-thread-js-helper", + }, + ], +]; + +/** + * This component manages the settings for recording a performance profile. + * @extends {React.PureComponent} + */ +class Settings extends PureComponent { + /** + * @param {Props} props + */ + constructor(props) { + super(props); + /** @type {State} */ + this.state = { + // Allow the textbox to have a temporary tracked value. + temporaryThreadText: null, + }; + + this._handleThreadCheckboxChange = this._handleThreadCheckboxChange.bind( + this + ); + this._handleFeaturesCheckboxChange = this._handleFeaturesCheckboxChange.bind( + this + ); + this._handleAddObjdir = this._handleAddObjdir.bind(this); + this._handleRemoveObjdir = this._handleRemoveObjdir.bind(this); + this._setThreadTextFromInput = this._setThreadTextFromInput.bind(this); + this._handleThreadTextCleanup = this._handleThreadTextCleanup.bind(this); + this._renderThreadsColumns = this._renderThreadsColumns.bind(this); + + this._intervalExponentialScale = makeExponentialScale(0.01, 100); + this._entriesExponentialScale = makePowerOf2Scale( + 128 * 1024, + 256 * 1024 * 1024 + ); + } + + /** + * Handle the checkbox change. + * @param {React.ChangeEvent} event + */ + _handleThreadCheckboxChange(event) { + const { threads, changeThreads } = this.props; + const { checked, value } = event.target; + + if (checked) { + if (!threads.includes(value)) { + changeThreads([...threads, value]); + } + } else { + changeThreads(threads.filter(thread => thread !== value)); + } + } + + /** + * Handle the checkbox change. + * @param {React.ChangeEvent} event + */ + _handleFeaturesCheckboxChange(event) { + const { features, changeFeatures } = this.props; + const { checked, value } = event.target; + + if (checked) { + if (!features.includes(value)) { + changeFeatures([value, ...features]); + } + } else { + changeFeatures(features.filter(feature => feature !== value)); + } + } + + _handleAddObjdir() { + const { objdirs, changeObjdirs } = this.props; + openFilePickerForObjdir(window, objdirs, changeObjdirs); + } + + /** + * @param {number} index + * @return {void} + */ + _handleRemoveObjdir(index) { + const { objdirs, changeObjdirs } = this.props; + const newObjdirs = [...objdirs]; + newObjdirs.splice(index, 1); + changeObjdirs(newObjdirs); + } + + /** + * @param {React.ChangeEvent} event + */ + _setThreadTextFromInput(event) { + this.setState({ temporaryThreadText: event.target.value }); + } + + /** + * @param {React.ChangeEvent} event + */ + _handleThreadTextCleanup(event) { + this.setState({ temporaryThreadText: null }); + this.props.changeThreads(_threadTextToList(event.target.value)); + } + + /** + * @param {ThreadColumn[]} threadDisplay + * @param {number} index + * @return {React.ReactNode} + */ + _renderThreadsColumns(threadDisplay, index) { + const { threads } = this.props; + return div( + { className: "perf-settings-thread-column", key: index }, + threadDisplay.map(({ name, id, l10nId }) => + Localized( + // The title is localized with a description of the thread. + { id: l10nId, attrs: { title: true }, key: name }, + label( + { + className: + "perf-settings-checkbox-label perf-settings-thread-label", + }, + input({ + className: "perf-settings-checkbox", + id: `perf-settings-thread-checkbox-${id}`, + type: "checkbox", + // Do not localize the value, this is used internally by the profiler. + value: name, + checked: threads.includes(name), + onChange: this._handleThreadCheckboxChange, + }), + name + ) + ) + ) + ); + } + _renderThreads() { + const { temporaryThreadText } = this.state; + const { threads } = this.props; + + return renderSection( + "perf-settings-threads-summary", + Localized({ id: "perftools-heading-threads" }, "Threads"), + div( + null, + div( + { className: "perf-settings-thread-columns" }, + threadColumns.map(this._renderThreadsColumns) + ), + div( + { className: "perf-settings-all-threads" }, + label( + { + className: "perf-settings-checkbox-label", + }, + input({ + className: "perf-settings-checkbox", + id: "perf-settings-thread-checkbox-all-threads", + type: "checkbox", + value: "*", + checked: threads.includes("*"), + onChange: this._handleThreadCheckboxChange, + }), + Localized({ id: "perftools-record-all-registered-threads" }) + ) + ), + div( + { className: "perf-settings-row" }, + Localized( + { id: "perftools-tools-threads-input-label" }, + label( + { className: "perf-settings-text-label" }, + div( + null, + Localized( + { id: "perftools-custom-threads-label" }, + "Add custom threads by name:" + ) + ), + input({ + className: "perf-settings-text-input", + id: "perftools-settings-thread-text", + type: "text", + value: + temporaryThreadText === null + ? threads.join(",") + : temporaryThreadText, + onBlur: this._handleThreadTextCleanup, + onFocus: this._setThreadTextFromInput, + onChange: this._setThreadTextFromInput, + }) + ) + ) + ) + ) + ); + } + + /** + * @param {React.ReactNode} sectionTitle + * @param {FeatureDescription[]} features + * @param {boolean} isSupported + */ + _renderFeatureSection(sectionTitle, features, isSupported) { + if (features.length === 0) { + return null; + } + + // Note: This area is not localized. This area is pretty deep in the UI, and is mostly + // geared towards Firefox engineers. It may not be worth localizing. This decision + // can be tracked in Bug 1682333. + + return div( + null, + h3(null, sectionTitle), + features.map(featureDescription => { + const { name, value, title, disabledReason } = featureDescription; + const extraClassName = isSupported + ? "" + : "perf-settings-checkbox-label-disabled"; + return label( + { + className: `perf-settings-checkbox-label perf-settings-feature-label ${extraClassName}`, + key: value, + }, + div( + { className: "perf-settings-checkbox-and-name" }, + input({ + className: "perf-settings-checkbox", + id: `perf-settings-feature-checkbox-${value}`, + type: "checkbox", + value, + checked: isSupported && this.props.features.includes(value), + onChange: this._handleFeaturesCheckboxChange, + disabled: !isSupported, + }), + div( + { className: "perf-settings-feature-name" }, + !isSupported && featureDescription.experimental + ? // Note when unsupported features are experimental. + `${name} (Experimental)` + : name + ) + ), + div( + { className: "perf-settings-feature-title" }, + title, + !isSupported && disabledReason + ? div( + { className: "perf-settings-feature-disabled-reason" }, + disabledReason + ) + : null + ) + ); + }) + ); + } + + _renderFeatures() { + const { supportedFeatures } = this.props; + + // Divvy up the features into their respective groups. + const recommended = []; + const supported = []; + const unsupported = []; + const experimental = []; + + for (const feature of featureDescriptions) { + if (supportedFeatures.includes(feature.value)) { + if (feature.experimental) { + experimental.push(feature); + } else if (feature.recommended) { + recommended.push(feature); + } else { + supported.push(feature); + } + } else { + unsupported.push(feature); + } + } + + return div( + { className: "perf-settings-sections" }, + div( + null, + this._renderFeatureSection( + Localized( + { id: "perftools-heading-features-default" }, + "Features (Recommended on by default)" + ), + recommended, + true + ), + this._renderFeatureSection( + Localized({ id: "perftools-heading-features" }, "Features"), + supported, + true + ), + this._renderFeatureSection( + Localized( + { id: "perftools-heading-features-experimental" }, + "Experimental" + ), + experimental, + true + ), + this._renderFeatureSection( + Localized( + { id: "perftools-heading-features-disabled" }, + "Disabled Features" + ), + unsupported, + false + ) + ) + ); + } + + _renderLocalBuildSection() { + const { objdirs } = this.props; + return renderSection( + "perf-settings-local-build-summary", + Localized({ id: "perftools-heading-local-build" }), + div( + null, + p(null, Localized({ id: "perftools-description-local-build" })), + DirectoryPicker({ + dirs: objdirs, + onAdd: this._handleAddObjdir, + onRemove: this._handleRemoveObjdir, + }) + ) + ); + } + + render() { + return section( + { className: "perf-settings" }, + h1(null, Localized({ id: "perftools-heading-settings" })), + h2( + { className: "perf-settings-title" }, + Localized({ id: "perftools-heading-buffer" }) + ), + Range({ + label: Localized({ id: "perftools-range-interval-label" }), + value: this.props.interval, + id: "perf-range-interval", + scale: this._intervalExponentialScale, + display: _intervalTextDisplay, + onChange: this.props.changeInterval, + }), + Range({ + label: Localized({ id: "perftools-range-entries-label" }), + value: this.props.entries, + id: "perf-range-entries", + scale: this._entriesExponentialScale, + display: _entriesTextDisplay, + onChange: this.props.changeEntries, + }), + this._renderThreads(), + this._renderFeatures(), + this._renderLocalBuildSection() + ); + } +} + +/** + * Clean up the thread list string into a list of values. + * @param {string} threads - Comma separated values. + * @return {string[]} + */ +function _threadTextToList(threads) { + return ( + threads + // Split on commas + .split(",") + // Clean up any extraneous whitespace + .map(string => string.trim()) + // Filter out any blank strings + .filter(string => string) + ); +} + +/** + * Format the interval number for display. + * @param {number} value + * @return {React.ReactNode} + */ +function _intervalTextDisplay(value) { + return Localized({ + id: "perftools-range-interval-milliseconds", + $interval: value, + }); +} + +/** + * Format the entries number for display. + * @param {number} value + * @return {string} + */ +function _entriesTextDisplay(value) { + return formatFileSize(value * PROFILE_ENTRY_SIZE); +} + +/** + * about:profiling doesn't need to collapse the children into details/summary, + * but the popup and devtools do (for now). + * + * @param {string} id + * @param {React.ReactNode} title + * @param {React.ReactNode} children + * @returns React.ReactNode + */ +function renderSection(id, title, children) { + return div( + { className: "perf-settings-sections" }, + div(null, h2(null, title), children) + ); +} + +/** + * @param {StoreState} state + * @returns {StateProps} + */ +function mapStateToProps(state) { + return { + interval: selectors.getInterval(state), + entries: selectors.getEntries(state), + features: selectors.getFeatures(state), + threads: selectors.getThreads(state), + threadsString: selectors.getThreadsString(state), + objdirs: selectors.getObjdirs(state), + supportedFeatures: selectors.getSupportedFeatures(state), + }; +} + +/** @type {ThunkDispatchProps} */ +const mapDispatchToProps = { + changeInterval: actions.changeInterval, + changeEntries: actions.changeEntries, + changeFeatures: actions.changeFeatures, + changeThreads: actions.changeThreads, + changeObjdirs: actions.changeObjdirs, +}; + +const SettingsConnected = connect( + mapStateToProps, + mapDispatchToProps +)(Settings); + +module.exports = SettingsConnected; diff --git a/devtools/client/performance-new/components/moz.build b/devtools/client/performance-new/components/moz.build new file mode 100644 index 0000000000..2287b44a7e --- /dev/null +++ b/devtools/client/performance-new/components/moz.build @@ -0,0 +1,18 @@ +# vim: set filetype=python: +# 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/. + +DevToolsModules( + "AboutProfiling.js", + "Description.js", + "DevToolsPanel.js", + "DevToolsPresetSelection.js", + "DirectoryPicker.js", + "OnboardingMessage.js", + "Presets.js", + "ProfilerEventHandling.js", + "Range.js", + "RecordingButton.js", + "Settings.js", +) -- cgit v1.2.3