AboutProfiling.js (10363B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 // @ts-check 5 6 /** 7 * @typedef {import("../../@types/perf").State} StoreState 8 * @typedef {import("../../@types/perf").PerformancePref} PerformancePref 9 */ 10 11 "use strict"; 12 13 const { 14 PureComponent, 15 createFactory, 16 createElement: h, 17 Fragment, 18 createRef, 19 } = require("resource://devtools/client/shared/vendor/react.mjs"); 20 const { 21 connect, 22 } = require("resource://devtools/client/shared/vendor/react-redux.js"); 23 const { 24 div, 25 h1, 26 button, 27 } = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 28 const Localized = createFactory( 29 require("resource://devtools/client/shared/vendor/fluent-react.js").Localized 30 ); 31 const Settings = createFactory( 32 require("resource://devtools/client/performance-new/components/aboutprofiling/Settings.js") 33 ); 34 const Presets = createFactory( 35 require("resource://devtools/client/performance-new/components/aboutprofiling/Presets.js") 36 ); 37 38 const selectors = require("resource://devtools/client/performance-new/store/selectors.js"); 39 const { 40 restartBrowserWithEnvironmentVariable, 41 } = require("resource://devtools/client/performance-new/shared/browser.js"); 42 43 /** @type {PerformancePref["AboutProfilingHasDeveloperOptions"]} */ 44 const ABOUTPROFILING_HAS_DEVELOPER_OPTIONS_PREF = 45 "devtools.performance.aboutprofiling.has-developer-options"; 46 47 /** 48 * This function encodes the parameter so that it can be used as an environment 49 * variable value. 50 * Basically it uses single quotes, but replacing any single quote by '"'"': 51 * 1. close the previous single-quoted string, 52 * 2. add a double-quoted string containing only a single quote 53 * 3. start a single-quoted string again. 54 * so that it's properly retained. 55 * 56 * @param {string} value 57 * @returns {string} 58 */ 59 function encodeShellValue(value) { 60 return "'" + value.replaceAll("'", `'"'"'`) + "'"; 61 } 62 63 /** 64 * @typedef {import("../../@types/perf").RecordingSettings} RecordingSettings 65 * 66 * @typedef {object} ButtonStateProps 67 * @property {RecordingSettings} recordingSettings 68 * 69 * @typedef {ButtonStateProps} ButtonProps 70 * 71 * @typedef {object} ButtonState 72 * @property {boolean} hasDeveloperOptions 73 */ 74 75 /** 76 * This component implements the button that triggers the menu that makes it 77 * possible to show more actions. 78 * 79 * @augments {React.PureComponent<ButtonProps, ButtonState>} 80 */ 81 class MoreActionsButtonImpl extends PureComponent { 82 state = { 83 hasDeveloperOptions: Services.prefs.getBoolPref( 84 ABOUTPROFILING_HAS_DEVELOPER_OPTIONS_PREF, 85 false 86 ), 87 }; 88 89 componentDidMount() { 90 Services.prefs.addObserver( 91 ABOUTPROFILING_HAS_DEVELOPER_OPTIONS_PREF, 92 this.onHasDeveloperOptionsPrefChanges 93 ); 94 } 95 96 componentWillUnmount() { 97 Services.prefs.removeObserver( 98 ABOUTPROFILING_HAS_DEVELOPER_OPTIONS_PREF, 99 this.onHasDeveloperOptionsPrefChanges 100 ); 101 } 102 _menuRef = createRef(); 103 104 onHasDeveloperOptionsPrefChanges = () => { 105 this.setState({ 106 hasDeveloperOptions: Services.prefs.getBoolPref( 107 ABOUTPROFILING_HAS_DEVELOPER_OPTIONS_PREF, 108 false 109 ), 110 }); 111 }; 112 113 /** 114 * See the part "Showing the menu" in 115 * https://searchfox.org/mozilla-central/rev/4bacdbc8ac088f2ee516daf42c535fab2bc24a04/toolkit/content/widgets/panel-list/README.stories.md 116 * Strangely our React's type doesn't have the `detail` property for 117 * MouseEvent, so we're defining it manually. 118 * 119 * @param {React.MouseEvent & { detail: number }} e 120 */ 121 handleClickOrMousedown = e => { 122 // The menu is toggled either for a "mousedown", or for a keyboard enter 123 // (which triggers a "click" event with 0 clicks (detail == 0)). 124 if (this._menuRef.current && (e.type == "mousedown" || e.detail === 0)) { 125 this._menuRef.current.toggle(e.nativeEvent, e.currentTarget); 126 } 127 }; 128 129 /** 130 * @returns {Record<string, string>} 131 */ 132 getEnvironmentVariablesForStartupFromRecordingSettings = () => { 133 const { interval, entries, threads, features } = 134 this.props.recordingSettings; 135 return { 136 MOZ_PROFILER_STARTUP: "1", 137 MOZ_PROFILER_STARTUP_INTERVAL: String(interval), 138 MOZ_PROFILER_STARTUP_ENTRIES: String(entries), 139 MOZ_PROFILER_STARTUP_FEATURES: features.join(","), 140 MOZ_PROFILER_STARTUP_FILTERS: threads.join(","), 141 }; 142 }; 143 144 onRestartWithProfiling = () => { 145 const envVariables = 146 this.getEnvironmentVariablesForStartupFromRecordingSettings(); 147 restartBrowserWithEnvironmentVariable(envVariables); 148 }; 149 150 onCopyEnvVariables = async () => { 151 const envVariables = 152 this.getEnvironmentVariablesForStartupFromRecordingSettings(); 153 const envString = Object.entries(envVariables) 154 .map(([key, value]) => `${key}=${encodeShellValue(value)}`) 155 .join(" "); 156 await navigator.clipboard.writeText(envString); 157 }; 158 159 onCopyTestVariables = async () => { 160 const { interval, entries, threads, features } = 161 this.props.recordingSettings; 162 163 const envString = 164 "--gecko-profile" + 165 ` --gecko-profile-interval ${interval}` + 166 ` --gecko-profile-entries ${entries}` + 167 ` --gecko-profile-features ${encodeShellValue(features.join(","))}` + 168 ` --gecko-profile-threads ${encodeShellValue(threads.join(","))}`; 169 await navigator.clipboard.writeText(envString); 170 }; 171 172 render() { 173 return h( 174 Fragment, 175 null, 176 Localized( 177 { 178 id: "perftools-menu-more-actions-button", 179 attrs: { title: true }, 180 }, 181 h("moz-button", { 182 iconsrc: "chrome://global/skin/icons/more.svg", 183 "aria-expanded": "false", 184 "aria-haspopup": "menu", 185 onClick: this.handleClickOrMousedown, 186 onMouseDown: this.handleClickOrMousedown, 187 }) 188 ), 189 h( 190 "panel-list", 191 { ref: this._menuRef }, 192 Localized( 193 { id: "perftools-menu-more-actions-restart-with-profiling" }, 194 h( 195 "panel-item", 196 { onClick: this.onRestartWithProfiling }, 197 "Restart Firefox with startup profiling enabled" 198 ) 199 ), 200 this.state.hasDeveloperOptions 201 ? Localized( 202 { id: "perftools-menu-more-actions-copy-for-startup" }, 203 h( 204 "panel-item", 205 { onClick: this.onCopyEnvVariables }, 206 "Copy environment variables for startup profiling" 207 ) 208 ) 209 : null, 210 this.state.hasDeveloperOptions 211 ? Localized( 212 { id: "perftools-menu-more-actions-copy-for-perf-tests" }, 213 h( 214 "panel-item", 215 { onClick: this.onCopyTestVariables }, 216 "Copy parameters for mach try perf" 217 ) 218 ) 219 : null 220 ) 221 ); 222 } 223 } 224 225 /** 226 * @param {StoreState} state 227 * @returns {ButtonStateProps} 228 */ 229 function mapStateToButtonProps(state) { 230 return { 231 recordingSettings: selectors.getRecordingSettings(state), 232 }; 233 } 234 const MoreActionsButton = connect(mapStateToButtonProps)(MoreActionsButtonImpl); 235 236 /** 237 * @typedef {import("../../@types/perf").PageContext} PageContext 238 * 239 * @typedef {object} StateProps 240 * @property {boolean?} isSupportedPlatform 241 * @property {PageContext} pageContext 242 * @property {string | null} promptEnvRestart 243 * @property {(() => void) | undefined} openRemoteDevTools 244 * 245 * @typedef {StateProps} Props 246 */ 247 248 /** 249 * This is the top level component for the about:profiling page. It shares components 250 * with the popup and DevTools page. 251 * 252 * @augments {React.PureComponent<Props>} 253 */ 254 class AboutProfiling extends PureComponent { 255 render() { 256 const { 257 isSupportedPlatform, 258 pageContext, 259 promptEnvRestart, 260 openRemoteDevTools, 261 } = this.props; 262 263 if (isSupportedPlatform === null) { 264 // We don't know yet if this is a supported platform, wait for a response. 265 return null; 266 } 267 268 return div( 269 { className: `perf perf-${pageContext}` }, 270 promptEnvRestart 271 ? div( 272 { className: "perf-env-restart" }, 273 div( 274 { 275 className: 276 "perf-photon-message-bar perf-photon-message-bar-warning perf-env-restart-fixed", 277 }, 278 div({ className: "perf-photon-message-bar-warning-icon" }), 279 Localized({ id: "perftools-status-restart-required" }), 280 button( 281 { 282 className: "perf-photon-button perf-photon-button-micro", 283 type: "button", 284 onClick: () => { 285 restartBrowserWithEnvironmentVariable({ 286 [promptEnvRestart]: "1", 287 }); 288 }, 289 }, 290 Localized({ id: "perftools-button-restart" }) 291 ) 292 ) 293 ) 294 : null, 295 296 openRemoteDevTools 297 ? div( 298 { className: "perf-back" }, 299 button( 300 { 301 className: "perf-back-button", 302 type: "button", 303 onClick: openRemoteDevTools, 304 }, 305 Localized({ id: "perftools-button-save-settings" }) 306 ) 307 ) 308 : null, 309 310 div( 311 { className: "perf-intro" }, 312 div( 313 { className: "perf-intro-title-bar" }, 314 h1( 315 { className: "perf-intro-title" }, 316 Localized({ id: "perftools-intro-title" }) 317 ), 318 h(MoreActionsButton) 319 ), 320 div( 321 { className: "perf-intro-row" }, 322 div({}, div({ className: "perf-intro-icon" })), 323 Localized({ 324 className: "perf-intro-text", 325 id: "perftools-intro-description", 326 }) 327 ) 328 ), 329 Presets(), 330 Settings() 331 ); 332 } 333 } 334 335 /** 336 * @param {StoreState} state 337 * @returns {StateProps} 338 */ 339 function mapStateToProps(state) { 340 return { 341 isSupportedPlatform: selectors.getIsSupportedPlatform(state), 342 pageContext: selectors.getPageContext(state), 343 promptEnvRestart: selectors.getPromptEnvRestart(state), 344 openRemoteDevTools: selectors.getOpenRemoteDevTools(state), 345 }; 346 } 347 348 module.exports = connect(mapStateToProps)(AboutProfiling);