diff options
Diffstat (limited to 'devtools/client/performance-new/components/aboutprofiling/Settings.js')
-rw-r--r-- | devtools/client/performance-new/components/aboutprofiling/Settings.js | 674 |
1 files changed, 674 insertions, 0 deletions
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; |