summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance-new/components
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /devtools/client/performance-new/components
parentInitial commit. (diff)
downloadfirefox-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-new/components')
-rw-r--r--devtools/client/performance-new/components/AboutProfiling.js161
-rw-r--r--devtools/client/performance-new/components/Description.js69
-rw-r--r--devtools/client/performance-new/components/DevToolsPanel.js83
-rw-r--r--devtools/client/performance-new/components/DevToolsPresetSelection.js204
-rw-r--r--devtools/client/performance-new/components/DirectoryPicker.js122
-rw-r--r--devtools/client/performance-new/components/OnboardingMessage.js144
-rw-r--r--devtools/client/performance-new/components/Presets.js163
-rw-r--r--devtools/client/performance-new/components/ProfilerEventHandling.js269
-rw-r--r--devtools/client/performance-new/components/README.md3
-rw-r--r--devtools/client/performance-new/components/Range.js79
-rw-r--r--devtools/client/performance-new/components/RecordingButton.js254
-rw-r--r--devtools/client/performance-new/components/Settings.js620
-rw-r--r--devtools/client/performance-new/components/moz.build18
13 files changed, 2189 insertions, 0 deletions
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<P>} ResolveThunks<P>
+ */
+
+/**
+ * @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<Props>}
+ */
+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<Props>}
+ */
+class Description extends PureComponent {
+ /**
+ * @param {Props} props
+ */
+ constructor(props) {
+ super(props);
+ this.handleLinkClick = this.handleLinkClick.bind(this);
+ }
+
+ /**
+ * @param {React.MouseEvent<HTMLButtonElement>} 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<P>} ResolveThunks<P>
+ */
+
+/**
+ * @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<Props>}
+ */
+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<P>} ResolveThunks<P>
+ */
+
+/**
+ * @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<ThunkDispatchProps>} 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<Props>}
+ */
+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<HTMLSelectElement>} 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<Props>}
+ */
+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<Props>}
+ */
+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<P>} ResolveThunks<P>
+ */
+
+"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<PresetProps>}
+ */
+class Preset extends PureComponent {
+ /**
+ * Handle the checkbox change.
+ * @param {React.ChangeEvent<HTMLInputElement>} 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<ThunkDispatchProps>} 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<Props>}
+ */
+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<P>} ResolveThunks<P>
+ */
+
+/**
+ * @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<ThunkDispatchProps>} 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<Props>}
+ */
+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<Props>
+ */
+class Range extends PureComponent {
+ /** @param {Props} props */
+ constructor(props) {
+ super(props);
+ this.handleInput = this.handleInput.bind(this);
+ }
+
+ /**
+ * @param {React.ChangeEvent<HTMLInputElement>} 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<P>} ResolveThunks<P>
+ */
+
+/**
+ * @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<ThunkDispatchProps>} 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<Props>}
+ */
+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<ThunkDispatchProps>} 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<P>} ResolveThunks<P>
+ */
+
+/**
+ * @template InjectedProps
+ * @template NeededProps
+ * @typedef {import("react-redux")
+ * .InferableComponentEnhancerWithProps<InjectedProps, NeededProps>
+ * } InferableComponentEnhancerWithProps<InjectedProps, NeededProps>
+ */
+"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<ThreadColumn[]>} */
+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<Props, State>}
+ */
+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<HTMLInputElement>} 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<HTMLInputElement>} 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<HTMLInputElement>} event
+ */
+ _setThreadTextFromInput(event) {
+ this.setState({ temporaryThreadText: event.target.value });
+ }
+
+ /**
+ * @param {React.ChangeEvent<HTMLInputElement>} 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",
+)