Settings.js (18386B)
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 {object} StateProps 8 * @property {number} interval 9 * @property {number} entries 10 * @property {string[]} features 11 * @property {string[]} threads 12 * @property {string} threadsString 13 * @property {string[]} objdirs 14 * @property {string[]} supportedFeatures 15 */ 16 17 /** 18 * @typedef {object} ThunkDispatchProps 19 * @property {typeof actions.changeInterval} changeInterval 20 * @property {typeof actions.changeEntries} changeEntries 21 * @property {typeof actions.changeFeatures} changeFeatures 22 * @property {typeof actions.changeThreads} changeThreads 23 * @property {typeof actions.changeObjdirs} changeObjdirs 24 */ 25 26 /** 27 * @typedef {ResolveThunks<ThunkDispatchProps>} DispatchProps 28 */ 29 30 /** 31 * @typedef {object} State 32 * @property {null | string} temporaryThreadText 33 */ 34 35 /** 36 * @typedef {import("../../@types/perf").State} StoreState 37 * @typedef {import("../../@types/perf").FeatureDescription} FeatureDescription 38 * 39 * @typedef {StateProps & DispatchProps} Props 40 */ 41 42 /** 43 * @template P 44 * @typedef {import("react-redux").ResolveThunks<P>} ResolveThunks<P> 45 */ 46 47 /** 48 * @template InjectedProps 49 * @template NeededProps 50 * @typedef {import("react-redux") 51 * .InferableComponentEnhancerWithProps<InjectedProps, NeededProps> 52 * } InferableComponentEnhancerWithProps<InjectedProps, NeededProps> 53 */ 54 "use strict"; 55 56 const { 57 PureComponent, 58 createFactory, 59 } = require("resource://devtools/client/shared/vendor/react.mjs"); 60 const { 61 code, 62 div, 63 label, 64 input, 65 h1, 66 h2, 67 h3, 68 section, 69 p, 70 span, 71 } = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 72 const Range = createFactory( 73 require("resource://devtools/client/performance-new/components/aboutprofiling/Range.js") 74 ); 75 const DirectoryPicker = createFactory( 76 require("resource://devtools/client/performance-new/components/aboutprofiling/DirectoryPicker.js") 77 ); 78 const { 79 makeLinear10Scale, 80 makePowerOf2Scale, 81 formatFileSize, 82 featureDescriptions, 83 } = require("resource://devtools/client/performance-new/shared/utils.js"); 84 const { 85 connect, 86 } = require("resource://devtools/client/shared/vendor/react-redux.js"); 87 const actions = require("resource://devtools/client/performance-new/store/actions.js"); 88 const selectors = require("resource://devtools/client/performance-new/store/selectors.js"); 89 const { 90 openFilePickerForObjdir, 91 } = require("resource://devtools/client/performance-new/shared/browser.js"); 92 const Localized = createFactory( 93 require("resource://devtools/client/shared/vendor/fluent-react.js").Localized 94 ); 95 96 // The Gecko Profiler interprets the "entries" setting as 8 bytes per entry. 97 const PROFILE_ENTRY_SIZE = 8; 98 99 /** 100 * @typedef {{ name: string, id: string, l10nId: string }} ThreadColumn 101 */ 102 103 /** @type {Array<ThreadColumn[]>} */ 104 const threadColumns = [ 105 [ 106 { 107 name: "GeckoMain", 108 id: "gecko-main", 109 // The l10nId take the form `perf-thread-${id}`, but isn't done programmatically 110 // so that it is easy to search in the codebase. 111 l10nId: "perftools-thread-gecko-main", 112 }, 113 { 114 name: "Compositor", 115 id: "compositor", 116 l10nId: "perftools-thread-compositor", 117 }, 118 { 119 name: "DOM Worker", 120 id: "dom-worker", 121 l10nId: "perftools-thread-dom-worker", 122 }, 123 { 124 name: "Renderer", 125 id: "renderer", 126 l10nId: "perftools-thread-renderer", 127 }, 128 ], 129 [ 130 { 131 name: "RenderBackend", 132 id: "render-backend", 133 l10nId: "perftools-thread-render-backend", 134 }, 135 { 136 name: "Timer", 137 id: "timer", 138 l10nId: "perftools-thread-timer", 139 }, 140 { 141 name: "StyleThread", 142 id: "style-thread", 143 l10nId: "perftools-thread-style-thread", 144 }, 145 { 146 name: "Socket Thread", 147 id: "socket-thread", 148 l10nId: "perftools-thread-socket-thread", 149 }, 150 ], 151 [ 152 { 153 name: "StreamTrans", 154 id: "stream-trans", 155 l10nId: "pref-thread-stream-trans", 156 }, 157 { 158 name: "ImgDecoder", 159 id: "img-decoder", 160 l10nId: "perftools-thread-img-decoder", 161 }, 162 { 163 name: "DNS Resolver", 164 id: "dns-resolver", 165 l10nId: "perftools-thread-dns-resolver", 166 }, 167 { 168 // Threads that are part of XPCOM's TaskController thread pool. 169 name: "TaskController", 170 id: "task-controller", 171 l10nId: "perftools-thread-task-controller", 172 }, 173 ], 174 ]; 175 176 /** @type {Array<ThreadColumn[]>} */ 177 const jvmThreadColumns = [ 178 [ 179 { 180 name: "Gecko", 181 id: "gecko", 182 l10nId: "perftools-thread-jvm-gecko", 183 }, 184 { 185 name: "Nimbus", 186 id: "nimbus", 187 l10nId: "perftools-thread-jvm-nimbus", 188 }, 189 ], 190 [ 191 { 192 name: "DefaultDispatcher", 193 id: "default-dispatcher", 194 l10nId: "perftools-thread-jvm-default-dispatcher", 195 }, 196 { 197 name: "Glean", 198 id: "glean", 199 l10nId: "perftools-thread-jvm-glean", 200 }, 201 ], 202 [ 203 { 204 name: "arch_disk_io", 205 id: "arch-disk-io", 206 l10nId: "perftools-thread-jvm-arch-disk-io", 207 }, 208 { 209 name: "pool-", 210 id: "pool", 211 l10nId: "perftools-thread-jvm-pool", 212 }, 213 ], 214 ]; 215 216 /** 217 * This component manages the settings for recording a performance profile. 218 * 219 * @augments {React.PureComponent<Props, State>} 220 */ 221 class Settings extends PureComponent { 222 /** 223 * @param {Props} props 224 */ 225 constructor(props) { 226 super(props); 227 /** @type {State} */ 228 this.state = { 229 // Allow the textbox to have a temporary tracked value. 230 temporaryThreadText: null, 231 }; 232 233 this._intervalExponentialScale = makeLinear10Scale(0.01, 1000); 234 this._entriesExponentialScale = makePowerOf2Scale( 235 128 * 1024, 236 256 * 1024 * 1024 237 ); 238 } 239 240 /** 241 * Handle the checkbox change. 242 * 243 * @param {React.ChangeEvent<HTMLInputElement>} event 244 */ 245 _handleThreadCheckboxChange = event => { 246 const { threads, changeThreads } = this.props; 247 const { checked, value } = event.target; 248 249 if (checked) { 250 if (!threads.includes(value)) { 251 changeThreads([...threads, value]); 252 } 253 } else { 254 changeThreads(threads.filter(thread => thread !== value)); 255 } 256 }; 257 258 /** 259 * Handle the checkbox change. 260 * 261 * @param {React.ChangeEvent<HTMLInputElement>} event 262 */ 263 _handleFeaturesCheckboxChange = event => { 264 const { features, changeFeatures } = this.props; 265 const { checked, value } = event.target; 266 267 if (checked) { 268 if (!features.includes(value)) { 269 changeFeatures([value, ...features]); 270 } 271 } else { 272 changeFeatures(features.filter(feature => feature !== value)); 273 } 274 }; 275 276 _handleAddObjdir = () => { 277 const { objdirs, changeObjdirs } = this.props; 278 openFilePickerForObjdir(window, objdirs, changeObjdirs); 279 }; 280 281 /** 282 * @param {number} index 283 * @return {void} 284 */ 285 _handleRemoveObjdir = index => { 286 const { objdirs, changeObjdirs } = this.props; 287 const newObjdirs = [...objdirs]; 288 newObjdirs.splice(index, 1); 289 changeObjdirs(newObjdirs); 290 }; 291 292 /** 293 * @param {React.ChangeEvent<HTMLInputElement>} event 294 */ 295 _setThreadTextFromInput = event => { 296 this.setState({ temporaryThreadText: event.target.value }); 297 }; 298 299 /** 300 * @param {React.ChangeEvent<HTMLInputElement>} event 301 */ 302 _handleThreadTextCleanup = event => { 303 this.setState({ temporaryThreadText: null }); 304 this.props.changeThreads(_threadTextToList(event.target.value)); 305 }; 306 307 /** 308 * @param {ThreadColumn[]} threadDisplay 309 * @param {number} index 310 * @return {React.ReactNode} 311 */ 312 _renderThreadsColumns(threadDisplay, index) { 313 const { threads } = this.props; 314 const areAllThreadsIncluded = threads.includes("*"); 315 return div( 316 { className: "perf-settings-thread-column", key: index }, 317 threadDisplay.map(({ name, id, l10nId }) => 318 Localized( 319 // The title is localized with a description of the thread. 320 { id: l10nId, attrs: { title: true }, key: name }, 321 label( 322 { 323 className: `perf-settings-checkbox-label perf-settings-thread-label toggle-container-with-text ${ 324 areAllThreadsIncluded 325 ? "perf-settings-checkbox-label-disabled" 326 : "" 327 }`, 328 }, 329 input({ 330 className: "perf-settings-checkbox", 331 id: `perf-settings-thread-checkbox-${id}`, 332 type: "checkbox", 333 // Do not localize the value, this is used internally by the profiler. 334 value: name, 335 checked: threads.includes(name), 336 disabled: areAllThreadsIncluded, 337 onChange: this._handleThreadCheckboxChange, 338 }), 339 span(null, name) 340 ) 341 ) 342 ) 343 ); 344 } 345 _renderThreads() { 346 const { temporaryThreadText } = this.state; 347 const { threads } = this.props; 348 349 return renderSection( 350 "perf-settings-threads-summary", 351 Localized({ id: "perftools-heading-threads" }, "Threads"), 352 div( 353 null, 354 div( 355 { className: "perf-settings-thread-columns" }, 356 threadColumns.map((threadDisplay, index) => 357 this._renderThreadsColumns(threadDisplay, index) 358 ) 359 ), 360 this._renderJvmThreads(), 361 div( 362 { 363 className: "perf-settings-checkbox-label perf-settings-all-threads", 364 }, 365 label( 366 { 367 className: "toggle-container-with-text", 368 }, 369 input({ 370 id: "perf-settings-thread-checkbox-all-threads", 371 type: "checkbox", 372 value: "*", 373 checked: threads.includes("*"), 374 onChange: this._handleThreadCheckboxChange, 375 }), 376 Localized({ id: "perftools-record-all-registered-threads" }) 377 ) 378 ), 379 div( 380 { className: "perf-settings-row" }, 381 Localized( 382 { id: "perftools-tools-threads-input-label" }, 383 label( 384 { className: "perf-settings-text-label" }, 385 div( 386 null, 387 Localized( 388 { id: "perftools-custom-threads-label" }, 389 "Add custom threads by name:" 390 ) 391 ), 392 input({ 393 className: "perf-settings-text-input", 394 id: "perftools-settings-thread-text", 395 type: "text", 396 value: 397 temporaryThreadText === null 398 ? threads.join(",") 399 : temporaryThreadText, 400 onBlur: this._handleThreadTextCleanup, 401 onFocus: this._setThreadTextFromInput, 402 onChange: this._setThreadTextFromInput, 403 }) 404 ) 405 ) 406 ) 407 ) 408 ); 409 } 410 411 _renderJvmThreads() { 412 if (!this.props.supportedFeatures.includes("java")) { 413 return null; 414 } 415 416 return [ 417 h2( 418 null, 419 Localized({ id: "perftools-heading-threads-jvm" }, "JVM Threads") 420 ), 421 div( 422 { className: "perf-settings-thread-columns" }, 423 jvmThreadColumns.map((threadDisplay, index) => 424 this._renderThreadsColumns(threadDisplay, index) 425 ) 426 ), 427 ]; 428 } 429 430 /** 431 * @param {React.ReactNode} sectionTitle 432 * @param {FeatureDescription[]} features 433 * @param {boolean} isSupported 434 */ 435 _renderFeatureSection(sectionTitle, features, isSupported) { 436 if (features.length === 0) { 437 return null; 438 } 439 440 // Note: This area is not localized. This area is pretty deep in the UI, and is mostly 441 // geared towards Firefox engineers. It may not be worth localizing. This decision 442 // can be tracked in Bug 1682333. 443 444 return div( 445 null, 446 h3(null, sectionTitle), 447 features.map(featureDescription => { 448 const { name, value, title, disabledReason } = featureDescription; 449 const extraClassName = isSupported 450 ? "" 451 : "perf-settings-checkbox-label-disabled"; 452 return label( 453 { 454 className: `perf-settings-checkbox-label perf-toggle-label ${extraClassName}`, 455 key: value, 456 }, 457 input({ 458 id: `perf-settings-feature-checkbox-${value}`, 459 type: "checkbox", 460 value, 461 checked: isSupported && this.props.features.includes(value), 462 onChange: this._handleFeaturesCheckboxChange, 463 disabled: !isSupported, 464 }), 465 div( 466 { className: "perf-toggle-text-label" }, 467 !isSupported && featureDescription.experimental 468 ? // Note when unsupported features are experimental. 469 `${name} (Experimental)` 470 : name, 471 span( 472 { className: "perf-toggle-feature-value" }, 473 "(", 474 code(null, value), 475 ")" 476 ) 477 ), 478 div( 479 { className: "perf-toggle-description" }, 480 title, 481 !isSupported && disabledReason 482 ? div( 483 { className: "perf-settings-feature-disabled-reason" }, 484 disabledReason 485 ) 486 : null 487 ) 488 ); 489 }) 490 ); 491 } 492 493 _renderFeatures() { 494 const { supportedFeatures } = this.props; 495 496 // Divvy up the features into their respective groups. 497 const recommended = []; 498 const supported = []; 499 const unsupported = []; 500 const experimental = []; 501 502 for (const feature of featureDescriptions) { 503 if (supportedFeatures.includes(feature.value)) { 504 if (feature.experimental) { 505 experimental.push(feature); 506 } else if (feature.recommended) { 507 recommended.push(feature); 508 } else { 509 supported.push(feature); 510 } 511 } else { 512 unsupported.push(feature); 513 } 514 } 515 516 return div( 517 { className: "perf-settings-sections" }, 518 div( 519 null, 520 this._renderFeatureSection( 521 Localized( 522 { id: "perftools-heading-features-default" }, 523 "Features (Recommended on by default)" 524 ), 525 recommended, 526 true 527 ), 528 this._renderFeatureSection( 529 Localized({ id: "perftools-heading-features" }, "Features"), 530 supported, 531 true 532 ), 533 this._renderFeatureSection( 534 Localized( 535 { id: "perftools-heading-features-experimental" }, 536 "Experimental" 537 ), 538 experimental, 539 true 540 ), 541 this._renderFeatureSection( 542 Localized( 543 { id: "perftools-heading-features-disabled" }, 544 "Disabled Features" 545 ), 546 unsupported, 547 false 548 ) 549 ) 550 ); 551 } 552 553 _renderLocalBuildSection() { 554 const { objdirs } = this.props; 555 return renderSection( 556 "perf-settings-local-build-summary", 557 Localized({ id: "perftools-heading-local-build" }), 558 div( 559 null, 560 p(null, Localized({ id: "perftools-description-local-build" })), 561 DirectoryPicker({ 562 dirs: objdirs, 563 onAdd: this._handleAddObjdir, 564 onRemove: this._handleRemoveObjdir, 565 }) 566 ) 567 ); 568 } 569 570 render() { 571 return section( 572 { className: "perf-settings" }, 573 h1(null, Localized({ id: "perftools-heading-settings" })), 574 h2( 575 { className: "perf-settings-title" }, 576 Localized({ id: "perftools-heading-buffer" }) 577 ), 578 Range({ 579 label: Localized({ id: "perftools-range-interval-label" }), 580 value: this.props.interval, 581 id: "perf-range-interval", 582 scale: this._intervalExponentialScale, 583 display: _intervalTextDisplay, 584 onChange: this.props.changeInterval, 585 }), 586 Range({ 587 label: Localized({ id: "perftools-range-entries-label" }), 588 value: this.props.entries, 589 id: "perf-range-entries", 590 scale: this._entriesExponentialScale, 591 display: _entriesTextDisplay, 592 onChange: this.props.changeEntries, 593 }), 594 this._renderThreads(), 595 this._renderFeatures(), 596 this._renderLocalBuildSection() 597 ); 598 } 599 } 600 601 /** 602 * Clean up the thread list string into a list of values. 603 * 604 * @param {string} threads - Comma separated values. 605 * @return {string[]} 606 */ 607 function _threadTextToList(threads) { 608 return ( 609 threads 610 // Split on commas 611 .split(",") 612 // Clean up any extraneous whitespace 613 .map(string => string.trim()) 614 // Filter out any blank strings 615 .filter(string => string) 616 ); 617 } 618 619 /** 620 * Format the interval number for display. 621 * 622 * @param {number} value 623 * @return {React.ReactNode} 624 */ 625 function _intervalTextDisplay(value) { 626 return Localized({ 627 id: "perftools-range-interval-milliseconds", 628 $interval: value, 629 }); 630 } 631 632 /** 633 * Format the entries number for display. 634 * 635 * @param {number} value 636 * @return {string} 637 */ 638 function _entriesTextDisplay(value) { 639 return formatFileSize(value * PROFILE_ENTRY_SIZE); 640 } 641 642 /** 643 * Renders a section for about:profiling. 644 * 645 * @param {string} id Unused. 646 * @param {React.ReactNode} title 647 * @param {React.ReactNode} children 648 * @returns React.ReactNode 649 */ 650 function renderSection(id, title, children) { 651 return div( 652 { className: "perf-settings-sections" }, 653 div(null, h2(null, title), children) 654 ); 655 } 656 657 /** 658 * @param {StoreState} state 659 * @returns {StateProps} 660 */ 661 function mapStateToProps(state) { 662 return { 663 interval: selectors.getInterval(state), 664 entries: selectors.getEntries(state), 665 features: selectors.getFeatures(state), 666 threads: selectors.getThreads(state), 667 threadsString: selectors.getThreadsString(state), 668 objdirs: selectors.getObjdirs(state), 669 supportedFeatures: selectors.getSupportedFeatures(state), 670 }; 671 } 672 673 /** @type {ThunkDispatchProps} */ 674 const mapDispatchToProps = { 675 changeInterval: actions.changeInterval, 676 changeEntries: actions.changeEntries, 677 changeFeatures: actions.changeFeatures, 678 changeThreads: actions.changeThreads, 679 changeObjdirs: actions.changeObjdirs, 680 }; 681 682 const SettingsConnected = connect( 683 mapStateToProps, 684 mapDispatchToProps 685 )(Settings); 686 687 module.exports = SettingsConnected;