1
0
Fork 0
firefox/devtools/client/performance-new/components/aboutprofiling/AboutProfiling.js
Daniel Baumann 5e9a113729
Adding upstream version 140.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-25 09:37:52 +02:00

346 lines
10 KiB
JavaScript

/* 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").State} StoreState
* @typedef {import("../../@types/perf").PerformancePref} PerformancePref
*/
"use strict";
const {
PureComponent,
createFactory,
createElement: h,
Fragment,
createRef,
} = require("resource://devtools/client/shared/vendor/react.mjs");
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");
/** @type {PerformancePref["AboutProfilingHasDeveloperOptions"]} */
const ABOUTPROFILING_HAS_DEVELOPER_OPTIONS_PREF =
"devtools.performance.aboutprofiling.has-developer-options";
/**
* This function encodes the parameter so that it can be used as an environment
* variable value.
* Basically it uses single quotes, but replacing any single quote by '"'"':
* 1. close the previous single-quoted string,
* 2. add a double-quoted string containing only a single quote
* 3. start a single-quoted string again.
* so that it's properly retained.
*
* @param {string} value
* @returns {string}
*/
function encodeShellValue(value) {
return "'" + value.replaceAll("'", `'"'"'`) + "'";
}
/**
* @typedef {import("../../@types/perf").RecordingSettings} RecordingSettings
*
* @typedef {Object} ButtonStateProps
* @property {RecordingSettings} recordingSettings
*
* @typedef {ButtonStateProps} ButtonProps
*
* @typedef {Object} ButtonState
* @property {boolean} hasDeveloperOptions
*/
/**
* This component implements the button that triggers the menu that makes it
* possible to show more actions.
* @extends {React.PureComponent<ButtonProps, ButtonState>}
*/
class MoreActionsButtonImpl extends PureComponent {
state = {
hasDeveloperOptions: Services.prefs.getBoolPref(
ABOUTPROFILING_HAS_DEVELOPER_OPTIONS_PREF,
false
),
};
componentDidMount() {
Services.prefs.addObserver(
ABOUTPROFILING_HAS_DEVELOPER_OPTIONS_PREF,
this.onHasDeveloperOptionsPrefChanges
);
}
componentWillUnmount() {
Services.prefs.removeObserver(
ABOUTPROFILING_HAS_DEVELOPER_OPTIONS_PREF,
this.onHasDeveloperOptionsPrefChanges
);
}
_menuRef = createRef();
onHasDeveloperOptionsPrefChanges = () => {
this.setState({
hasDeveloperOptions: Services.prefs.getBoolPref(
ABOUTPROFILING_HAS_DEVELOPER_OPTIONS_PREF,
false
),
});
};
/**
* See the part "Showing the menu" in
* https://searchfox.org/mozilla-central/rev/4bacdbc8ac088f2ee516daf42c535fab2bc24a04/toolkit/content/widgets/panel-list/README.stories.md
* Strangely our React's type doesn't have the `detail` property for
* MouseEvent, so we're defining it manually.
* @param {React.MouseEvent & { detail: number }} e
*/
handleClickOrMousedown = e => {
// The menu is toggled either for a "mousedown", or for a keyboard enter
// (which triggers a "click" event with 0 clicks (detail == 0)).
if (this._menuRef.current && (e.type == "mousedown" || e.detail === 0)) {
this._menuRef.current.toggle(e.nativeEvent, e.currentTarget);
}
};
/**
* @returns {Record<string, string>}
*/
getEnvironmentVariablesForStartupFromRecordingSettings = () => {
const { interval, entries, threads, features } =
this.props.recordingSettings;
return {
MOZ_PROFILER_STARTUP: "1",
MOZ_PROFILER_STARTUP_INTERVAL: String(interval),
MOZ_PROFILER_STARTUP_ENTRIES: String(entries),
MOZ_PROFILER_STARTUP_FEATURES: features.join(","),
MOZ_PROFILER_STARTUP_FILTERS: threads.join(","),
};
};
onRestartWithProfiling = () => {
const envVariables =
this.getEnvironmentVariablesForStartupFromRecordingSettings();
restartBrowserWithEnvironmentVariable(envVariables);
};
onCopyEnvVariables = async () => {
const envVariables =
this.getEnvironmentVariablesForStartupFromRecordingSettings();
const envString = Object.entries(envVariables)
.map(([key, value]) => `${key}=${encodeShellValue(value)}`)
.join(" ");
await navigator.clipboard.writeText(envString);
};
onCopyTestVariables = async () => {
const { interval, entries, threads, features } =
this.props.recordingSettings;
const envString =
"--gecko-profile" +
` --gecko-profile-interval ${interval}` +
` --gecko-profile-entries ${entries}` +
` --gecko-profile-features ${encodeShellValue(features.join(","))}` +
` --gecko-profile-threads ${encodeShellValue(threads.join(","))}`;
await navigator.clipboard.writeText(envString);
};
render() {
return h(
Fragment,
null,
Localized(
{
id: "perftools-menu-more-actions-button",
attrs: { title: true },
},
h("moz-button", {
iconsrc: "chrome://global/skin/icons/more.svg",
"aria-expanded": "false",
"aria-haspopup": "menu",
onClick: this.handleClickOrMousedown,
onMouseDown: this.handleClickOrMousedown,
})
),
h(
"panel-list",
{ ref: this._menuRef },
Localized(
{ id: "perftools-menu-more-actions-restart-with-profiling" },
h(
"panel-item",
{ onClick: this.onRestartWithProfiling },
"Restart Firefox with startup profiling enabled"
)
),
this.state.hasDeveloperOptions
? Localized(
{ id: "perftools-menu-more-actions-copy-for-startup" },
h(
"panel-item",
{ onClick: this.onCopyEnvVariables },
"Copy environment variables for startup profiling"
)
)
: null,
this.state.hasDeveloperOptions
? Localized(
{ id: "perftools-menu-more-actions-copy-for-perf-tests" },
h(
"panel-item",
{ onClick: this.onCopyTestVariables },
"Copy parameters for mach try perf"
)
)
: null
)
);
}
}
/**
* @param {StoreState} state
* @returns {ButtonStateProps}
*/
function mapStateToButtonProps(state) {
return {
recordingSettings: selectors.getRecordingSettings(state),
};
}
const MoreActionsButton = connect(mapStateToButtonProps)(MoreActionsButtonImpl);
/**
* @typedef {import("../../@types/perf").PageContext} PageContext
*
* @typedef {Object} StateProps
* @property {boolean?} isSupportedPlatform
* @property {PageContext} pageContext
* @property {string | null} promptEnvRestart
* @property {(() => void) | undefined} openRemoteDevTools
*
* @typedef {StateProps} Props
*/
/**
* 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 {
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: () => {
restartBrowserWithEnvironmentVariable({
[promptEnvRestart]: "1",
});
},
},
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" },
div(
{ className: "perf-intro-title-bar" },
h1(
{ className: "perf-intro-title" },
Localized({ id: "perftools-intro-title" })
),
h(MoreActionsButton)
),
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);