summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance-new/components
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/performance-new/components')
-rw-r--r--devtools/client/performance-new/components/README.md3
-rw-r--r--devtools/client/performance-new/components/aboutprofiling/AboutProfiling.js156
-rw-r--r--devtools/client/performance-new/components/aboutprofiling/DirectoryPicker.js119
-rw-r--r--devtools/client/performance-new/components/aboutprofiling/Presets.js167
-rw-r--r--devtools/client/performance-new/components/aboutprofiling/Range.js78
-rw-r--r--devtools/client/performance-new/components/aboutprofiling/Settings.js674
-rw-r--r--devtools/client/performance-new/components/aboutprofiling/moz.build12
-rw-r--r--devtools/client/performance-new/components/moz.build10
-rw-r--r--devtools/client/performance-new/components/panel/Description.js63
-rw-r--r--devtools/client/performance-new/components/panel/DevToolsPanel.js108
-rw-r--r--devtools/client/performance-new/components/panel/DevToolsPresetSelection.js219
-rw-r--r--devtools/client/performance-new/components/panel/OnboardingMessage.js138
-rw-r--r--devtools/client/performance-new/components/panel/ProfilerEventHandling.js128
-rw-r--r--devtools/client/performance-new/components/panel/RecordingButton.js265
-rw-r--r--devtools/client/performance-new/components/panel/ToolboxHighlightController.js61
-rw-r--r--devtools/client/performance-new/components/panel/moz.build14
-rw-r--r--devtools/client/performance-new/components/shared/ProfilerPreferenceObserver.js110
-rw-r--r--devtools/client/performance-new/components/shared/moz.build8
18 files changed, 2333 insertions, 0 deletions
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/aboutprofiling/AboutProfiling.js b/devtools/client/performance-new/components/aboutprofiling/AboutProfiling.js
new file mode 100644
index 0000000000..0d205093ba
--- /dev/null
+++ b/devtools/client/performance-new/components/aboutprofiling/AboutProfiling.js
@@ -0,0 +1,156 @@
+/* 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").PageContext} PageContext
+ */
+
+"use strict";
+
+const {
+ PureComponent,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+const {
+ div,
+ h1,
+ button,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const Localized = createFactory(
+ require("resource://devtools/client/shared/vendor/fluent-react.js").Localized
+);
+const Settings = createFactory(
+ require("resource://devtools/client/performance-new/components/aboutprofiling/Settings.js")
+);
+const Presets = createFactory(
+ require("resource://devtools/client/performance-new/components/aboutprofiling/Presets.js")
+);
+
+const selectors = require("resource://devtools/client/performance-new/store/selectors.js");
+const {
+ restartBrowserWithEnvironmentVariable,
+} = require("resource://devtools/client/performance-new/shared/browser.js");
+
+/**
+ * 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 {
+ 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/aboutprofiling/DirectoryPicker.js b/devtools/client/performance-new/components/aboutprofiling/DirectoryPicker.js
new file mode 100644
index 0000000000..4d9ea8373d
--- /dev/null
+++ b/devtools/client/performance-new/components/aboutprofiling/DirectoryPicker.js
@@ -0,0 +1,119 @@
+/* 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("resource://devtools/client/shared/vendor/react.js");
+const {
+ div,
+ button,
+ select,
+ option,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const {
+ withCommonPathPrefixRemoved,
+} = require("resource://devtools/client/performance-new/shared/utils.js");
+const Localized = createFactory(
+ require("resource://devtools/client/shared/vendor/fluent-react.js").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;
+ }
+
+ /**
+ * @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/aboutprofiling/Presets.js b/devtools/client/performance-new/components/aboutprofiling/Presets.js
new file mode 100644
index 0000000000..4dc97d7e32
--- /dev/null
+++ b/devtools/client/performance-new/components/aboutprofiling/Presets.js
@@ -0,0 +1,167 @@
+/* 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,
+ createFactory,
+ Fragment,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ div,
+ label,
+ input,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const selectors = require("resource://devtools/client/performance-new/store/selectors.js");
+const actions = require("resource://devtools/client/performance-new/store/actions.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const Localized = createFactory(
+ require("resource://devtools/client/shared/vendor/fluent-react.js").Localized
+);
+
+/**
+ * @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;
+ const presetLabelAndDescription = preset
+ ? createElement(
+ Fragment,
+ null,
+ Localized(
+ { id: preset.l10nIds.devtools.label },
+ div({ className: "perf-toggle-text-label" })
+ ),
+ Localized(
+ { id: preset.l10nIds.devtools.description },
+ div({ className: "perf-toggle-description" })
+ )
+ )
+ : Localized(
+ { id: "perftools-presets-custom-label" },
+ div({ className: "perf-toggle-text-label" }, "Custom")
+ );
+
+ return label(
+ { className: "perf-toggle-label" },
+ input({
+ className: "perf-presets-radio-button",
+ type: "radio",
+ name: "presets",
+ value: presetName,
+ checked: selected,
+ onChange: this.onChange,
+ }),
+ presetLabelAndDescription
+ );
+ }
+}
+
+/**
+ * @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 {
+ /**
+ * 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/aboutprofiling/Range.js b/devtools/client/performance-new/components/aboutprofiling/Range.js
new file mode 100644
index 0000000000..ce787ba334
--- /dev/null
+++ b/devtools/client/performance-new/components/aboutprofiling/Range.js
@@ -0,0 +1,78 @@
+/* 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("resource://devtools/client/shared/vendor/react.js");
+const {
+ div,
+ input,
+ label,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+/**
+ * Provide a numeric range slider UI that works off of custom numeric scales.
+ * @extends React.PureComponent<Props>
+ */
+class Range extends PureComponent {
+ /**
+ * @param {React.ChangeEvent<HTMLInputElement>} event
+ */
+ handleInput = event => {
+ event.preventDefault();
+ const { scale, onChange } = this.props;
+ const frac = Number(event.target.value) / scale.steps;
+ onChange(scale.fromFractionToSingleDigitValue(frac));
+ };
+
+ render() {
+ const { label: labelText, scale, id, value, display } = this.props;
+
+ const min = "0";
+ const max = scale.steps;
+ // Convert the value to the current range.
+ const rangeValue = scale.fromValueToFraction(value) * max;
+
+ return div(
+ { className: "perf-settings-range-row" },
+ label(
+ {
+ className: "perf-settings-label",
+ htmlFor: id,
+ },
+ labelText
+ ),
+ input({
+ type: "range",
+ className: `perf-settings-range-input`,
+ min,
+ "aria-valuemin": scale.fromFractionToValue(0),
+ max,
+ "aria-valuemax": scale.fromFractionToValue(1),
+ value: rangeValue,
+ "aria-valuenow": value,
+ onChange: this.handleInput,
+ id,
+ }),
+ div({ className: `perf-settings-range-value` }, display(value))
+ );
+ }
+}
+
+module.exports = Range;
diff --git a/devtools/client/performance-new/components/aboutprofiling/Settings.js b/devtools/client/performance-new/components/aboutprofiling/Settings.js
new file mode 100644
index 0000000000..3afe3a2d94
--- /dev/null
+++ b/devtools/client/performance-new/components/aboutprofiling/Settings.js
@@ -0,0 +1,674 @@
+/* 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").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("resource://devtools/client/shared/vendor/react.js");
+const {
+ div,
+ label,
+ input,
+ h1,
+ h2,
+ h3,
+ section,
+ p,
+ span,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const Range = createFactory(
+ require("resource://devtools/client/performance-new/components/aboutprofiling/Range.js")
+);
+const DirectoryPicker = createFactory(
+ require("resource://devtools/client/performance-new/components/aboutprofiling/DirectoryPicker.js")
+);
+const {
+ makeLinear10Scale,
+ makePowerOf2Scale,
+ formatFileSize,
+ featureDescriptions,
+} = require("resource://devtools/client/performance-new/shared/utils.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+const actions = require("resource://devtools/client/performance-new/store/actions.js");
+const selectors = require("resource://devtools/client/performance-new/store/selectors.js");
+const {
+ openFilePickerForObjdir,
+} = require("resource://devtools/client/performance-new/shared/browser.js");
+const Localized = createFactory(
+ require("resource://devtools/client/shared/vendor/fluent-react.js").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: "Timer",
+ id: "timer",
+ l10nId: "perftools-thread-timer",
+ },
+ {
+ 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-img-decoder",
+ },
+ {
+ name: "DNS Resolver",
+ id: "dns-resolver",
+ l10nId: "perftools-thread-dns-resolver",
+ },
+ {
+ // Threads that are part of XPCOM's TaskController thread pool.
+ name: "TaskController",
+ id: "task-controller",
+ l10nId: "perftools-thread-task-controller",
+ },
+ ],
+];
+
+/** @type {Array<ThreadColumn[]>} */
+const jvmThreadColumns = [
+ [
+ {
+ name: "Gecko",
+ id: "gecko",
+ l10nId: "perftools-thread-jvm-gecko",
+ },
+ {
+ name: "Nimbus",
+ id: "nimbus",
+ l10nId: "perftools-thread-jvm-nimbus",
+ },
+ ],
+ [
+ {
+ name: "DefaultDispatcher",
+ id: "default-dispatcher",
+ l10nId: "perftools-thread-jvm-default-dispatcher",
+ },
+ {
+ name: "Glean",
+ id: "glean",
+ l10nId: "perftools-thread-jvm-glean",
+ },
+ ],
+ [
+ {
+ name: "arch_disk_io",
+ id: "arch-disk-io",
+ l10nId: "perftools-thread-jvm-arch-disk-io",
+ },
+ {
+ name: "pool-",
+ id: "pool",
+ l10nId: "perftools-thread-jvm-pool",
+ },
+ ],
+];
+
+/**
+ * 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._intervalExponentialScale = makeLinear10Scale(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;
+ const areAllThreadsIncluded = threads.includes("*");
+ 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 toggle-container-with-text ${
+ areAllThreadsIncluded
+ ? "perf-settings-checkbox-label-disabled"
+ : ""
+ }`,
+ },
+ 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),
+ disabled: areAllThreadsIncluded,
+ onChange: this._handleThreadCheckboxChange,
+ }),
+ span(null, 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((threadDisplay, index) =>
+ this._renderThreadsColumns(threadDisplay, index)
+ )
+ ),
+ this._renderJvmThreads(),
+ div(
+ {
+ className: "perf-settings-checkbox-label perf-settings-all-threads",
+ },
+ label(
+ {
+ className: "toggle-container-with-text",
+ },
+ input({
+ 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,
+ })
+ )
+ )
+ )
+ )
+ );
+ }
+
+ _renderJvmThreads() {
+ if (!this.props.supportedFeatures.includes("java")) {
+ return null;
+ }
+
+ return [
+ h2(
+ null,
+ Localized({ id: "perftools-heading-threads-jvm" }, "JVM Threads")
+ ),
+ div(
+ { className: "perf-settings-thread-columns" },
+ jvmThreadColumns.map((threadDisplay, index) =>
+ this._renderThreadsColumns(threadDisplay, index)
+ )
+ ),
+ ];
+ }
+
+ /**
+ * @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-toggle-label ${extraClassName}`,
+ key: value,
+ },
+ input({
+ id: `perf-settings-feature-checkbox-${value}`,
+ type: "checkbox",
+ value,
+ checked: isSupported && this.props.features.includes(value),
+ onChange: this._handleFeaturesCheckboxChange,
+ disabled: !isSupported,
+ }),
+ div(
+ { className: "perf-toggle-text-label" },
+ !isSupported && featureDescription.experimental
+ ? // Note when unsupported features are experimental.
+ `${name} (Experimental)`
+ : name
+ ),
+ div(
+ { className: "perf-toggle-description" },
+ 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);
+}
+
+/**
+ * Renders a section for about:profiling.
+ *
+ * @param {string} id Unused.
+ * @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/aboutprofiling/moz.build b/devtools/client/performance-new/components/aboutprofiling/moz.build
new file mode 100644
index 0000000000..792041682e
--- /dev/null
+++ b/devtools/client/performance-new/components/aboutprofiling/moz.build
@@ -0,0 +1,12 @@
+# 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",
+ "DirectoryPicker.js",
+ "Presets.js",
+ "Range.js",
+ "Settings.js",
+)
diff --git a/devtools/client/performance-new/components/moz.build b/devtools/client/performance-new/components/moz.build
new file mode 100644
index 0000000000..1f621838fd
--- /dev/null
+++ b/devtools/client/performance-new/components/moz.build
@@ -0,0 +1,10 @@
+# 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/.
+
+DIRS += [
+ "aboutprofiling",
+ "panel",
+ "shared",
+]
diff --git a/devtools/client/performance-new/components/panel/Description.js b/devtools/client/performance-new/components/panel/Description.js
new file mode 100644
index 0000000000..f2fb3620f0
--- /dev/null
+++ b/devtools/client/performance-new/components/panel/Description.js
@@ -0,0 +1,63 @@
+/* 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("resource://devtools/client/shared/vendor/react.js");
+const {
+ div,
+ button,
+ p,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const Localized = createFactory(
+ require("resource://devtools/client/shared/vendor/fluent-react.js").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 {React.MouseEvent<HTMLButtonElement>} event
+ */
+ handleLinkClick = event => {
+ const {
+ openDocLink,
+ } = require("resource://devtools/client/shared/link.js");
+
+ /** @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/panel/DevToolsPanel.js b/devtools/client/performance-new/components/panel/DevToolsPanel.js
new file mode 100644
index 0000000000..8f1b8fb344
--- /dev/null
+++ b/devtools/client/performance-new/components/panel/DevToolsPanel.js
@@ -0,0 +1,108 @@
+/* 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 {Object} OwnProps
+ * @property {import("../../@types/perf").PerfFront} perfFront
+ * @property {import("../../@types/perf").OnProfileReceived} onProfileReceived
+ * @property {() => void} onEditSettingsLinkClicked
+ */
+
+/**
+ * @typedef {StateProps & OwnProps} Props
+ * @typedef {import("../../@types/perf").State} StoreState
+ * @typedef {import("../../@types/perf").RecordingState} RecordingState
+ * @typedef {import("../../@types/perf").PanelWindow} PanelWindow
+ */
+
+"use strict";
+
+const {
+ PureComponent,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+const {
+ div,
+ hr,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const RecordingButton = createFactory(
+ require("resource://devtools/client/performance-new/components/panel/RecordingButton.js")
+);
+const Description = createFactory(
+ require("resource://devtools/client/performance-new/components/panel/Description.js")
+);
+const DevToolsPresetSelection = createFactory(
+ require("resource://devtools/client/performance-new/components/panel/DevToolsPresetSelection.js")
+);
+const OnboardingMessage = createFactory(
+ require("resource://devtools/client/performance-new/components/panel/OnboardingMessage.js")
+);
+const ToolboxHighlightController = createFactory(
+ require("resource://devtools/client/performance-new/components/panel/ToolboxHighlightController.js")
+);
+
+const selectors = require("resource://devtools/client/performance-new/store/selectors.js");
+const anyWindow = /** @type {any} */ (window);
+const panelWindow = /** @type {PanelWindow} */ (anyWindow);
+
+/**
+ * This is the top level component for the DevTools panel.
+ *
+ * @extends {React.PureComponent<Props>}
+ */
+class DevToolsPanel extends PureComponent {
+ render() {
+ const {
+ isSupportedPlatform,
+ perfFront,
+ onProfileReceived,
+ onEditSettingsLinkClicked,
+ } = this.props;
+
+ if (isSupportedPlatform === null) {
+ // We don't know yet if this is a supported platform, wait for a response.
+ return null;
+ }
+
+ return [
+ OnboardingMessage(),
+ div(
+ { className: `perf perf-devtools` },
+ RecordingButton({ perfFront, onProfileReceived }),
+ panelWindow.gToolbox
+ ? ToolboxHighlightController({ toolbox: panelWindow.gToolbox })
+ : null,
+ Description(),
+ hr({ className: "perf-presets-hr" }),
+ DevToolsPresetSelection({ onEditSettingsLinkClicked })
+ ),
+ ];
+ }
+}
+
+/**
+ * @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/panel/DevToolsPresetSelection.js b/devtools/client/performance-new/components/panel/DevToolsPresetSelection.js
new file mode 100644
index 0000000000..8b6cd44727
--- /dev/null
+++ b/devtools/client/performance-new/components/panel/DevToolsPresetSelection.js
@@ -0,0 +1,219 @@
+/* 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 {import("../../@types/perf").Presets} presets
+ */
+
+/**
+ * @typedef {Object} ThunkDispatchProps
+ * @property {typeof actions.changePreset} changePreset
+ */
+
+/**
+ * @typedef {Object} OwnProps
+ * @property {() => void} onEditSettingsLinkClicked
+ */
+
+/**
+ * @typedef {ResolveThunks<ThunkDispatchProps>} DispatchProps
+ * @typedef {StateProps & DispatchProps & OwnProps} Props
+ * @typedef {import("../../@types/perf").State} StoreState
+ * @typedef {import("../../@types/perf").FeatureDescription} FeatureDescription
+ */
+
+"use strict";
+
+const {
+ PureComponent,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ div,
+ select,
+ option,
+ button,
+ ul,
+ li,
+ span,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+const actions = require("resource://devtools/client/performance-new/store/actions.js");
+const selectors = require("resource://devtools/client/performance-new/store/selectors.js");
+const {
+ featureDescriptions,
+} = require("resource://devtools/client/performance-new/shared/utils.js");
+const Localized = createFactory(
+ require("resource://devtools/client/shared/vendor/fluent-react.js").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);
+
+ /**
+ * 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, onEditSettingsLinkClicked } = this.props;
+
+ let presetDescription;
+ const currentPreset = presets[presetName];
+ if (currentPreset) {
+ // Display the current preset's description.
+ presetDescription = Localized({
+ id: currentPreset.l10nIds.devtools.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: "perf-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
+ );
+ })
+ )
+ );
+ }
+
+ 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]) =>
+ Localized(
+ { id: preset.l10nIds.devtools.label },
+ option({ key: name, value: name })
+ )
+ ),
+ Localized(
+ { id: "perftools-presets-custom-label" },
+ option({ value: "custom" })
+ )
+ )
+ // The overhead component will go here.
+ ),
+ div(
+ { className: "perf-presets-details-row perf-presets-description" },
+ presetDescription
+ ),
+ button(
+ {
+ className: "perf-external-link",
+ onClick: onEditSettingsLinkClicked,
+ },
+ Localized({ id: "perftools-button-edit-settings" })
+ )
+ )
+ );
+ }
+}
+
+/**
+ * @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),
+ };
+}
+
+/**
+ * @type {ThunkDispatchProps}
+ */
+const mapDispatchToProps = {
+ changePreset: actions.changePreset,
+};
+
+module.exports = connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(DevToolsPresetSelection);
diff --git a/devtools/client/performance-new/components/panel/OnboardingMessage.js b/devtools/client/performance-new/components/panel/OnboardingMessage.js
new file mode 100644
index 0000000000..614215c6e6
--- /dev/null
+++ b/devtools/client/performance-new/components/panel/OnboardingMessage.js
@@ -0,0 +1,138 @@
+/* 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,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ b,
+ button,
+ div,
+ p,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const Localized = createFactory(
+ require("resource://devtools/client/shared/vendor/fluent-react.js").Localized
+);
+
+const { openDocLink } = require("resource://devtools/client/shared/link.js");
+
+const LEARN_MORE_URL = "https://profiler.firefox.com/docs";
+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, {});
+ };
+
+ /**
+ * 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;
+ }
+
+ /** @type {any} */
+ const anyWindow = window;
+
+ // If gToolbox is not defined on window, the component is rendered in
+ // about:debugging, and no onboarding message should be displayed.
+ if (!anyWindow.gToolbox) {
+ return null;
+ }
+
+ const learnMoreLink = button({
+ className: "perf-external-link",
+ onClick: this.handleLearnMoreClick,
+ });
+
+ const closeButton = Localized(
+ {
+ id: "perftools-onboarding-close-button",
+ attrs: { "aria-label": true },
+ },
+ button({
+ 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" },
+ Localized(
+ {
+ id: "perftools-onboarding-message",
+ b: b(),
+ a: learnMoreLink,
+ },
+ p({ className: "perf-onboarding-message-row" })
+ )
+ ),
+ closeButton
+ );
+ }
+}
+
+module.exports = OnboardingMessage;
diff --git a/devtools/client/performance-new/components/panel/ProfilerEventHandling.js b/devtools/client/performance-new/components/panel/ProfilerEventHandling.js
new file mode 100644
index 0000000000..1b415bdf19
--- /dev/null
+++ b/devtools/client/performance-new/components/panel/ProfilerEventHandling.js
@@ -0,0 +1,128 @@
+/* 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").PerfFront} PerfFront
+ * @typedef {import("../../@types/perf").RecordingState} RecordingState
+ * @typedef {import("../../@types/perf").State} StoreState
+ * @typedef {import("../../@types/perf").RootTraits} RootTraits
+ * @typedef {import("../../@types/perf").PanelWindow} PanelWindow
+ */
+
+/**
+ * @template P
+ * @typedef {import("react-redux").ResolveThunks<P>} ResolveThunks<P>
+ */
+
+/**
+ * @typedef {Object} StateProps
+ * @property {RecordingState} recordingState
+ * @property {boolean?} isSupportedPlatform
+ */
+
+/**
+ * @typedef {Object} ThunkDispatchProps
+ * @property {typeof actions.reportProfilerReady} reportProfilerReady
+ * @property {typeof actions.reportProfilerStarted} reportProfilerStarted
+ * @property {typeof actions.reportProfilerStopped} reportProfilerStopped
+ */
+
+/**
+ * @typedef {Object} OwnProps
+ * @property {PerfFront} perfFront
+ * @property {RootTraits} traits
+ */
+
+/**
+ * @typedef {ResolveThunks<ThunkDispatchProps>} DispatchProps
+ * @typedef {StateProps & DispatchProps & OwnProps} Props
+ */
+
+"use strict";
+
+const {
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+const actions = require("resource://devtools/client/performance-new/store/actions.js");
+const selectors = require("resource://devtools/client/performance-new/store/selectors.js");
+
+/**
+ * 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 {
+ componentDidMount() {
+ const {
+ perfFront,
+ isSupportedPlatform,
+ reportProfilerReady,
+ reportProfilerStarted,
+ reportProfilerStopped,
+ } = this.props;
+
+ if (!isSupportedPlatform) {
+ return;
+ }
+
+ // Ask for the initial state of the profiler.
+ perfFront.isActive().then(isActive => reportProfilerReady(isActive));
+
+ // Handle when the profiler changes state. It might be us, it might be someone else.
+ this.props.perfFront.on("profiler-started", reportProfilerStarted);
+ this.props.perfFront.on("profiler-stopped", reportProfilerStopped);
+ }
+
+ 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":
+ // 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.");
+ }
+ }
+
+ render() {
+ return null;
+ }
+}
+
+/**
+ * @param {StoreState} state
+ * @returns {StateProps}
+ */
+function mapStateToProps(state) {
+ return {
+ recordingState: selectors.getRecordingState(state),
+ isSupportedPlatform: selectors.getIsSupportedPlatform(state),
+ };
+}
+
+/** @type {ThunkDispatchProps} */
+const mapDispatchToProps = {
+ reportProfilerReady: actions.reportProfilerReady,
+ reportProfilerStarted: actions.reportProfilerStarted,
+ reportProfilerStopped: actions.reportProfilerStopped,
+};
+
+module.exports = connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(ProfilerEventHandling);
diff --git a/devtools/client/performance-new/components/panel/RecordingButton.js b/devtools/client/performance-new/components/panel/RecordingButton.js
new file mode 100644
index 0000000000..352468cbd1
--- /dev/null
+++ b/devtools/client/performance-new/components/panel/RecordingButton.js
@@ -0,0 +1,265 @@
+/* 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 | null} isSupportedPlatform
+ * @property {boolean} recordingUnexpectedlyStopped
+ * @property {PageContext} pageContext
+ */
+
+/**
+ * @typedef {Object} OwnProps
+ * @property {import("../../@types/perf").OnProfileReceived} onProfileReceived
+ * @property {import("../../@types/perf").PerfFront} perfFront
+ */
+
+/**
+ * @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 & OwnProps} Props
+ * @typedef {import("../../@types/perf").RecordingState} RecordingState
+ * @typedef {import("../../@types/perf").State} StoreState
+ * @typedef {import("../../@types/perf").PageContext} PageContext
+ */
+
+"use strict";
+
+const {
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ div,
+ button,
+ span,
+ img,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+const actions = require("resource://devtools/client/performance-new/store/actions.js");
+const selectors = require("resource://devtools/client/performance-new/store/selectors.js");
+const React = require("resource://devtools/client/shared/vendor/react.js");
+const Localized = React.createFactory(
+ require("resource://devtools/client/shared/vendor/fluent-react.js").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 {
+ _onStartButtonClick = () => {
+ const { startRecording, perfFront } = this.props;
+ startRecording(perfFront);
+ };
+
+ _onCaptureButtonClick = async () => {
+ const { getProfileAndStopProfiler, onProfileReceived, perfFront } =
+ this.props;
+ const profile = await getProfileAndStopProfiler(perfFront);
+ onProfileReceived(profile);
+ };
+
+ _onStopButtonClick = () => {
+ const { stopProfilerAndDiscardProfile, perfFront } = this.props;
+ stopProfilerAndDiscardProfile(perfFront);
+ };
+
+ render() {
+ const {
+ recordingState,
+ isSupportedPlatform,
+ recordingUnexpectedlyStopped,
+ } = 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: this._onStartButtonClick,
+ 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: this._onCaptureButtonClick,
+ disabled: recordingState === "request-to-start-recording",
+ additionalButton: {
+ label: Localized(
+ { id: "perftools-button-cancel-recording" },
+ "Cancel recording"
+ ),
+ onClick: this._onStopButtonClick,
+ },
+ });
+
+ 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/panel/ToolboxHighlightController.js b/devtools/client/performance-new/components/panel/ToolboxHighlightController.js
new file mode 100644
index 0000000000..9d9d594aa0
--- /dev/null
+++ b/devtools/client/performance-new/components/panel/ToolboxHighlightController.js
@@ -0,0 +1,61 @@
+/* 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 {RecordingState} recordingState
+ */
+
+/**
+ * @typedef {Object} OwnProps
+ * @property {any} toolbox
+ */
+
+/**
+ * @typedef {StateProps & OwnProps} Props
+ * @typedef {import("../../@types/perf").State} StoreState
+ * @typedef {import("../../@types/perf").RecordingState} RecordingState
+ */
+
+"use strict";
+
+const {
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+const selectors = require("resource://devtools/client/performance-new/store/selectors.js");
+
+/**
+ * @extends {React.PureComponent<Props>}
+ */
+class ToolboxHighlightController extends PureComponent {
+ /** @param {Props} prevProps */
+ componentDidUpdate(prevProps) {
+ const { recordingState, toolbox } = this.props;
+ if (recordingState === "recording") {
+ toolbox.highlightTool("performance");
+ } else if (prevProps.recordingState === "recording") {
+ toolbox.unhighlightTool("performance");
+ }
+ }
+
+ render() {
+ return null;
+ }
+}
+
+/**
+ * @param {StoreState} state
+ * @returns {StateProps}
+ */
+function mapStateToProps(state) {
+ return {
+ recordingState: selectors.getRecordingState(state),
+ };
+}
+
+module.exports = connect(mapStateToProps)(ToolboxHighlightController);
diff --git a/devtools/client/performance-new/components/panel/moz.build b/devtools/client/performance-new/components/panel/moz.build
new file mode 100644
index 0000000000..4063f5d333
--- /dev/null
+++ b/devtools/client/performance-new/components/panel/moz.build
@@ -0,0 +1,14 @@
+# 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(
+ "Description.js",
+ "DevToolsPanel.js",
+ "DevToolsPresetSelection.js",
+ "OnboardingMessage.js",
+ "ProfilerEventHandling.js",
+ "RecordingButton.js",
+ "ToolboxHighlightController.js",
+)
diff --git a/devtools/client/performance-new/components/shared/ProfilerPreferenceObserver.js b/devtools/client/performance-new/components/shared/ProfilerPreferenceObserver.js
new file mode 100644
index 0000000000..24a3eecbc5
--- /dev/null
+++ b/devtools/client/performance-new/components/shared/ProfilerPreferenceObserver.js
@@ -0,0 +1,110 @@
+/* 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 {import("../../@types/perf").State} StoreState
+ */
+
+/**
+ * @typedef {Object} StateProps
+ * @property {import("../../@types/perf").RecordingSettings} recordingSettingsFromRedux
+ * @property {import("../../@types/perf").PageContext} pageContext
+ * @property {string[]} supportedFeatures
+ */
+
+/**
+ * @typedef {Object} ThunkDispatchProps
+ * @property {typeof actions.updateSettingsFromPreferences} updateSettingsFromPreferences
+ */
+
+/**
+ * @typedef {ResolveThunks<ThunkDispatchProps>} DispatchProps
+ * @typedef {StateProps & DispatchProps} Props
+ */
+
+"use strict";
+
+// These functions live in a JSM file so that this can be used both by this
+// CommonJS DevTools environment and the popup which isn't in such a context.
+const {
+ getRecordingSettings,
+ setRecordingSettings,
+ addPrefObserver,
+ removePrefObserver,
+} = ChromeUtils.importESModule(
+ "resource://devtools/client/performance-new/shared/background.sys.mjs"
+);
+const {
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const selectors = require("resource://devtools/client/performance-new/store/selectors.js");
+const actions = require("resource://devtools/client/performance-new/store/actions.js");
+
+/**
+ * This component mirrors the settings in the redux store and the preferences in
+ * Firefox.
+ *
+ * @extends {React.PureComponent<Props>}
+ */
+class ProfilerPreferenceObserver extends PureComponent {
+ componentDidMount() {
+ this._updateSettingsFromPreferences();
+ addPrefObserver(this._updateSettingsFromPreferences);
+ }
+
+ componentDidUpdate() {
+ const { recordingSettingsFromRedux, pageContext } = this.props;
+ setRecordingSettings(pageContext, recordingSettingsFromRedux);
+ }
+
+ componentWillUnmount() {
+ removePrefObserver(this._updateSettingsFromPreferences);
+ }
+
+ _updateSettingsFromPreferences = () => {
+ const { updateSettingsFromPreferences, pageContext, supportedFeatures } =
+ this.props;
+
+ const recordingSettingsFromPrefs = getRecordingSettings(
+ pageContext,
+ supportedFeatures
+ );
+ updateSettingsFromPreferences(recordingSettingsFromPrefs);
+ };
+
+ render() {
+ return null;
+ }
+}
+
+/**
+ * @param {StoreState} state
+ * @returns {StateProps}
+ */
+function mapStateToProps(state) {
+ return {
+ recordingSettingsFromRedux: selectors.getRecordingSettings(state),
+ pageContext: selectors.getPageContext(state),
+ supportedFeatures: selectors.getSupportedFeatures(state),
+ };
+}
+
+const mapDispatchToProps = {
+ updateSettingsFromPreferences: actions.updateSettingsFromPreferences,
+};
+
+module.exports = connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(ProfilerPreferenceObserver);
diff --git a/devtools/client/performance-new/components/shared/moz.build b/devtools/client/performance-new/components/shared/moz.build
new file mode 100644
index 0000000000..8c7fc05b79
--- /dev/null
+++ b/devtools/client/performance-new/components/shared/moz.build
@@ -0,0 +1,8 @@
+# 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(
+ "ProfilerPreferenceObserver.js",
+)