Source code
Revision control
Copy as Markdown
Other Tools
/* 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
// @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, 1000);
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
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;