Bug 1450709 - Add profile recording settings to the recording panel; r?julienw draft
authorGreg Tatum <gtatum@mozilla.com>
Tue, 10 Apr 2018 13:13:49 -0500
changeset 781353 605008899a01d6f0d106a1e61da40737d3d8d06e
parent 780587 cfe6399e142c71966ef58a16cfd52c0b46dc6b1e
child 781933 3383aee90a7d8f4582784bdd9b2d108c5eb92db1
child 781934 c42ad86582a1929d0a38d73b5def1b55496edd09
child 781935 e2dce08b0df77f9be2b29c2813663e25090144d8
push id106276
push usergtatum@mozilla.com
push dateThu, 12 Apr 2018 20:54:48 +0000
reviewersjulienw
bugs1450709
milestone61.0a1
Bug 1450709 - Add profile recording settings to the recording panel; r?julienw MozReview-Commit-ID: HcdBkUMowMG
devtools/client/performance-new/components/Perf.js
devtools/client/performance-new/components/PerfSettings.js
devtools/client/performance-new/components/Range.js
devtools/client/performance-new/components/moz.build
devtools/client/performance-new/initializer.js
devtools/client/performance-new/moz.build
devtools/client/performance-new/panel.js
devtools/client/performance-new/utils.js
devtools/client/themes/perf.css
--- a/devtools/client/performance-new/components/Perf.js
+++ b/devtools/client/performance-new/components/Perf.js
@@ -1,16 +1,18 @@
 /* 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/. */
 "use strict";
 
-const { PureComponent } = require("devtools/client/shared/vendor/react");
-const { div, button } = require("devtools/client/shared/vendor/react-dom-factories");
+const { PureComponent, createFactory } = require("devtools/client/shared/vendor/react");
+const { div, button, p, span, img } = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const PerfSettings = createFactory(require("devtools/client/performance-new/components/PerfSettings.js"));
+const { openLink } = require("devtools/client/shared/link");
 
 /**
  * The recordingState is one of the following:
  **/
 
 // The initial state before we've queried the PerfActor
 const NOT_YET_KNOWN = "not-yet-known";
 // The profiler is available, we haven't started recording yet.
@@ -27,16 +29,17 @@ const RECORDING = "recording";
 // Some other code with access to the profiler started it.
 const OTHER_IS_RECORDING = "other-is-recording";
 // Profiling is not available when in private browsing mode.
 const LOCKED_BY_PRIVATE_BROWSING = "locked-by-private-browsing";
 
 class Perf extends PureComponent {
   static get propTypes() {
     return {
+      toolbox: PropTypes.object.isRequired,
       perfFront: PropTypes.object.isRequired,
       receiveProfile: PropTypes.func.isRequired
     };
   }
 
   constructor(props) {
     super(props);
     this.state = {
@@ -47,16 +50,18 @@ class Perf extends PureComponent {
     };
     this.startRecording = this.startRecording.bind(this);
     this.getProfileAndStopProfiler = this.getProfileAndStopProfiler.bind(this);
     this.stopProfilerAndDiscardProfile = this.stopProfilerAndDiscardProfile.bind(this);
     this.handleProfilerStarting = this.handleProfilerStarting.bind(this);
     this.handleProfilerStopping = this.handleProfilerStopping.bind(this);
     this.handlePrivateBrowsingStarting = this.handlePrivateBrowsingStarting.bind(this);
     this.handlePrivateBrowsingEnding = this.handlePrivateBrowsingEnding.bind(this);
+    this.settingsComponentCreated = this.settingsComponentCreated.bind(this);
+    this.handleLinkClick = this.handleLinkClick.bind(this);
   }
 
   componentDidMount() {
     const { perfFront } = this.props;
 
     // Ask for the initial state of the profiler.
     Promise.all([
       perfFront.isActive(),
@@ -109,16 +114,25 @@ class Perf extends PureComponent {
         this.props.perfFront.stopProfilerAndDiscardProfile();
         break;
 
       default:
         throw new Error("Unhandled recording state.");
     }
   }
 
+  /**
+   * Store a reference to the settings component. This gives the <Perf> component
+   * access to the `.getRecordingSettings()` method. At this time the recording panel
+   * is not doing much state management, so this avoid the overhead of redux.
+   */
+  settingsComponentCreated(settings) {
+    this.settings = settings;
+  }
+
   getRecordingStateForTesting() {
     return this.state.recordingState;
   }
 
   handleProfilerStarting() {
     switch (this.state.recordingState) {
       case NOT_YET_KNOWN:
         // We couldn't have started it yet, so it must have been someone
@@ -230,66 +244,74 @@ class Perf extends PureComponent {
     // be the only logical state to go into.
     this.setState({
       recordingState: AVAILABLE_TO_RECORD,
       recordingUnexpectedlyStopped: false
     });
   }
 
   startRecording() {
+    const settings = this.settings;
+    if (!settings) {
+      console.error("Expected the PerfSettings panel to be rendered and available.");
+      return;
+    }
     this.setState({
       recordingState: REQUEST_TO_START_RECORDING,
       // Reset this error state since it's no longer valid.
       recordingUnexpectedlyStopped: false,
     });
-    this.props.perfFront.startProfiler();
+    this.props.perfFront.startProfiler(
+      // Pull out the recording settings from the child component. This approach avoids
+      // using Redux as a state manager.
+      settings.getRecordingSettings()
+    );
   }
 
   async getProfileAndStopProfiler() {
     this.setState({ recordingState: REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER });
     const profile = await this.props.perfFront.getProfileAndStopProfiler();
     this.setState({ recordingState: AVAILABLE_TO_RECORD });
     console.log("getProfileAndStopProfiler");
     this.props.receiveProfile(profile);
   }
 
   stopProfilerAndDiscardProfile() {
     this.setState({ recordingState: REQUEST_TO_STOP_PROFILER });
     this.props.perfFront.stopProfilerAndDiscardProfile();
   }
 
-  render() {
+  renderButton() {
     const { recordingState, isSupportedPlatform } = this.state;
 
-    // Handle the cases of platform support.
-    switch (isSupportedPlatform) {
-      case null:
-        // We don't know yet if this is a supported platform, wait for a response.
-        return null;
-      case false:
-        return renderButton({
-          label: "Start recording",
-          disabled: true,
-          additionalMessage: "Your platform is not supported. The Gecko Profiler only " +
-                             "supports Tier-1 platforms."
-        });
-      case true:
-        // Continue on and render the panel.
-        break;
+    if (!isSupportedPlatform) {
+      return renderButton({
+        label: "Start recording",
+        disabled: true,
+        additionalMessage: "Your platform is not supported. The Gecko Profiler only " +
+                           "supports Tier-1 platforms."
+      });
     }
 
     // TODO - L10N all of the messages. Bug 1418056
     switch (recordingState) {
       case NOT_YET_KNOWN:
         return null;
 
       case AVAILABLE_TO_RECORD:
         return renderButton({
           onClick: this.startRecording,
-          label: "Start recording",
+          label: span(
+            null,
+            img({
+              className: "perf-button-image",
+              src: "chrome://devtools/skin/images/tool-profiler.svg"
+            }),
+            "Start recording",
+          ),
           additionalMessage: this.state.recordingUnexpectedlyStopped
             ? div(null, "The recording was stopped by another tool.")
             : null
         });
 
       case REQUEST_TO_STOP_PROFILER:
         return renderButton({
           label: "Stopping the recording",
@@ -324,26 +346,78 @@ class Perf extends PureComponent {
           additionalMessage: `The profiler is disabled when Private Browsing is enabled.
                               Close all Private Windows to re-enable the profiler`
         });
 
       default:
         throw new Error("Unhandled recording state");
     }
   }
+
+  handleLinkClick(event) {
+    openLink(event.target.value, this.props.toolbox);
+  }
+
+  render() {
+    const { isSupportedPlatform } = this.state;
+
+    if (isSupportedPlatform === null) {
+      // We don't know yet if this is a supported platform, wait for a response.
+      return null;
+    }
+
+    return div(
+      { className: "perf" },
+      this.renderButton(),
+      PerfSettings({ ref: this.settingsComponentCreated }),
+      div(
+        { className: "perf-description" },
+        p(null,
+          "This new recording panel is a bit different from the existing " +
+            "performance panel. It records the entire browser, and then opens up " +
+            "and shares the profile with ",
+          button(
+            // Implement links as buttons to avoid any risk of loading the link in the
+            // the panel.
+            {
+              className: "perf-external-link",
+              value: "https://perf-html.io",
+              onClick: this.handleLinkClick
+            },
+            "perf-html.io"
+          ),
+          ", a Mozilla performance analysis tool."
+        ),
+        p(null,
+          "This is still a prototype. Join along or file bugs at: ",
+          button(
+            // Implement links as buttons to avoid any risk of loading the link in the
+            // the panel.
+            {
+              className: "perf-external-link",
+              value: "https://github.com/devtools-html/perf.html",
+              onClick: this.handleLinkClick
+            },
+            "github.com/devtools-html/perf.html"
+          ),
+          "."
+        )
+      ),
+    );
+  }
 }
 
 module.exports = Perf;
 
 function renderButton(props) {
   const { disabled, label, onClick, additionalMessage } = props;
   const nbsp = "\u00A0";
 
   return div(
-    { className: "perf" },
+    { className: "perf-button-container" },
     div({ className: "perf-additional-message" }, additionalMessage || nbsp),
     div(
       null,
       button(
         {
           className: "devtools-button perf-button",
           "data-standalone": true,
           disabled,
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/components/PerfSettings.js
@@ -0,0 +1,409 @@
+/* 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/. */
+"use strict";
+const { PureComponent, createFactory } = require("devtools/client/shared/vendor/react");
+const { div, details, summary, label, input, span, h2, section } = require("devtools/client/shared/vendor/react-dom-factories");
+const Range = createFactory(require("devtools/client/performance-new/components/Range"));
+const { makeExponentialScale, formatFileSize, calculateOverhead } = require("devtools/client/performance-new/utils");
+
+// sizeof(double) + sizeof(char)
+// http://searchfox.org/mozilla-central/rev/e8835f52eff29772a57dca7bcc86a9a312a23729/tools/profiler/core/ProfileEntry.h#73
+const PROFILE_ENTRY_SIZE = 9;
+
+const NOTCHES = Array(22).fill("discrete-level-notch");
+
+const threadColumns = [
+  [
+    {
+      name: "GeckoMain",
+      title: "The main processes for both the parent process, and content processes"
+    },
+    {
+      name: "Compositor",
+      title: "Composites together different painted elements on the page."
+    },
+    {
+      name: "DOM Worker",
+      title: "This handle both web workers and service workers"
+    },
+    {
+      name: "Renderer",
+      title: "When WebRender is enabled, the thread that executes OpenGL calls"
+    },
+  ],
+  [
+    {
+      name: "RenderBackend",
+      title: "The WebRender RenderBackend thread"
+    },
+    {
+      name: "PaintWorker",
+      title: "When off-main-thread painting is enabled, the thread on which " +
+        "painting happens"
+    },
+    {
+      name: "StyleThread",
+      title: "Style computation is split into multiple threads"
+    },
+    {
+      name: "Socket Thread",
+      title: "The thread where networking code runs any blocking socket calls"
+    },
+  ],
+  [
+    {
+      name: "StreamTrans",
+      title: "TODO"
+    },
+    {
+      name: "ImgDecoder",
+      title: "Image decoding threads"
+    },
+    {
+      name: "DNS Resolver",
+      title: "DNS resolution happens on this thread"
+    },
+  ]
+];
+
+const featureCheckboxes = [
+  {
+    name: "Native Stacks",
+    value: "stackwalk",
+    title: "Record native stacks (C++ and Rust). This is not available on all platforms.",
+    recommended: true
+  },
+  {
+    name: "JavaScript",
+    value: "js",
+    title: "Record JavaScript stack information, and interleave it with native stacks.",
+    recommended: true
+  },
+  {
+    name: "Java",
+    value: "java",
+    title: "Profile Java code (Android only)."
+  },
+  {
+    name: "Native Leaf Stack",
+    value: "leaf",
+    title: "Record the native memory address of the leaf-most stack. This could be " +
+      "useful on platforms that do not support stack walking."
+  },
+  {
+    name: "Main Thread IO",
+    value: "mainthreadio",
+    title: "Record main thread I/O markers."
+  },
+  {
+    name: "Memory",
+    value: "memory",
+    title: "Add memory measurements to the samples, this includes resident set size " +
+      "(RSS) and unique set size (USS)."
+  },
+  {
+    name: "Privacy",
+    value: "privacy",
+    title: "Remove some potentially user-identifiable information."
+  },
+  {
+    name: "JIT Optimizations",
+    value: "trackopts",
+    title: "Track JIT optimizations in the JS engine."
+  },
+  {
+    name: "TaskTracer",
+    value: "tasktracer",
+    title: "Enable TaskTracer (Experimental, requires custom build.)"
+  },
+];
+
+/**
+ * This component manages the settings for recording a performance profile. In addition
+ * to rendering the UI, it also manages the state of the settings. In order to not
+ * introduce the complexity of adding Redux to a relatively simple UI, this
+ * component expects to be accessed via the `ref`, and then calling
+ * `settings.getRecordingSettings()` to get out the settings. If the recording panel
+ * takes on new responsibilities, then this decision should be revisited.
+ */
+class PerfSettings extends PureComponent {
+  static get propTypes() {
+    return {};
+  }
+
+  constructor(props) {
+    super(props);
+    // Right now the defaults are reset every time the panel is opened. These should
+    // be persisted between sessions. See Bug 1453014.
+    this.state = {
+      interval: 1,
+      entries: 10000000, // 90MB
+      features: {
+        js: true,
+        stackwalk: true,
+      },
+      threads: "GeckoMain,Compositor",
+      threadListFocused: false,
+    };
+    this._handleThreadCheckboxChange = this._handleThreadCheckboxChange.bind(this);
+    this._handleFeaturesCheckboxChange = this._handleFeaturesCheckboxChange.bind(this);
+    this._handleThreadTextChange = this._handleThreadTextChange.bind(this);
+    this._handleThreadTextCleanup = this._handleThreadTextCleanup.bind(this);
+    this._renderThreadsColumns = this._renderThreadsColumns.bind(this);
+    this._onChangeInterval = this._onChangeInterval.bind(this);
+    this._onChangeEntries = this._onChangeEntries.bind(this);
+    this._intervalExponentialScale = makeExponentialScale(0.01, 100);
+    this._entriesExponentialScale = makeExponentialScale(100000, 100000000);
+  }
+
+  getRecordingSettings() {
+    const features = [];
+    for (const [name, isSet] of Object.entries(this.state.features)) {
+      if (isSet) {
+        features.push(name);
+      }
+    }
+    return {
+      entries: this.state.entries,
+      interval: this.state.interval,
+      features,
+      threads: _threadStringToList(this.state.threads)
+    };
+  }
+
+  _renderNotches() {
+    const { interval, entries, features } = this.state;
+    const overhead = calculateOverhead(interval, entries, features);
+    const notchCount = 22;
+    const notches = [];
+    for (let i = 0; i < notchCount; i++) {
+      const active = i <= Math.round(overhead * (NOTCHES.length - 1))
+        ? "active" : "inactive";
+
+      let level = "normal";
+      if (i > 16) {
+        level = "critical";
+      } else if (i > 10) {
+        level = "warning";
+      }
+      notches.push(
+        div({
+          key: i,
+          className:
+          `perf-settings-notch perf-settings-notch-${level} ` +
+            `perf-settings-notch-${active}`
+        })
+      );
+    }
+    return notches;
+  }
+
+  _handleThreadCheckboxChange(event) {
+    const { checked, value }  = event.target;
+
+    this.setState(state => {
+      let threadsList = _threadStringToList(state.threads);
+      if (checked) {
+        if (!threadsList.includes(value)) {
+          threadsList.push(value);
+        }
+      } else {
+        threadsList = threadsList.filter(thread => thread !== value);
+      }
+      return { threads: threadsList.join(",") };
+    });
+  }
+
+  _handleFeaturesCheckboxChange(event) {
+    const { checked, value }  = event.target;
+
+    this.setState(state => ({
+      features: {...state.features, [value]: checked}
+    }));
+  }
+
+  _handleThreadTextChange(event) {
+    this.setState({ threads: event.target.value });
+  }
+
+  _handleThreadTextCleanup() {
+    this.setState(state => {
+      const threadsList = _threadStringToList(state.threads);
+      return { threads: threadsList.join(",") };
+    });
+  }
+
+  _renderThreadsColumns(threads, index) {
+    return div(
+      { className: "perf-settings-thread-column", key: index },
+      threads.map(({name, title}) => label(
+        {
+          className: "perf-settings-checkbox-label",
+          key: name,
+          title
+        },
+        input({
+          className: "perf-settings-checkbox",
+          type: "checkbox",
+          value: name,
+          checked: this.state.threads.includes(name),
+          onChange: this._handleThreadCheckboxChange
+        }),
+        name
+      ))
+    );
+  }
+
+  _renderThreads() {
+    return details(
+      { className: "perf-settings-details" },
+      summary({ className: "perf-settings-summary" }, "Threads:"),
+      // Contain the overflow of the slide down animation with the first div.
+      div(
+        { className: "perf-settings-details-contents" },
+        // Provide a second <div> element for the contents of the slide down animation.
+        div(
+          { className: "perf-settings-details-contents-slider" },
+          div(
+            { className: "perf-settings-thread-columns" },
+            threadColumns.map(this._renderThreadsColumns),
+          ),
+          div(
+            { className: "perf-settings-row" },
+            label(
+              {
+                className: "perf-settings-text-label",
+                title: "These thread names are a comma separated list that is used to " +
+                  "enable profiling of the threads in the profiler. The name needs to " +
+                  "be only a partial match of the thread name to be included. It " +
+                  "is whitespace sensitive."
+              },
+              div({}, "Add custom threads by name:"),
+              input({
+                className: "perf-settings-text-input",
+                type: "text",
+                value: this.state.threads,
+                onChange: this._handleThreadTextChange,
+                onBlur: this._handleThreadTextCleanup,
+              })
+            )
+          )
+        )
+      )
+    );
+  }
+
+  _renderFeatures() {
+    return details(
+      { className: "perf-settings-details" },
+      summary({ className: "perf-settings-summary" }, "Features:"),
+      div(
+        { className: "perf-settings-details-contents" },
+        div(
+          { className: "perf-settings-details-contents-slider" },
+          featureCheckboxes.map(({name, value, title, recommended}) => label(
+            {
+              className: "perf-settings-checkbox-label perf-settings-feature-label",
+              key: value,
+            },
+            input({
+              className: "perf-settings-checkbox",
+              type: "checkbox",
+              value,
+              checked: this.state.features[value],
+              onChange: this._handleFeaturesCheckboxChange
+            }),
+            div({ className: "perf-settings-feature-name" }, name),
+            div(
+              { className: "perf-settings-feature-title" },
+              title,
+              recommended
+                ? span(
+                  { className: "perf-settings-subtext" },
+                  " (Recommended on by default.)"
+                )
+                : null
+            )
+          ))
+        )
+      )
+    );
+  }
+
+  _onChangeInterval(interval) {
+    this.setState({ interval });
+  }
+
+  _onChangeEntries(entries) {
+    this.setState({ entries });
+  }
+
+  render() {
+    return section(
+      { className: "perf-settings" },
+      h2({ className: "perf-settings-title" }, "Recording Settings"),
+      div(
+        { className: "perf-settings-row" },
+        label({ className: "perf-settings-label" }, "Overhead:"),
+        div(
+          { className: "perf-settings-value perf-settings-notches" },
+          this._renderNotches()
+        )
+      ),
+      Range({
+        label: "Sampling interval:",
+        value: this.state.interval,
+        id: "perf-range-interval",
+        scale: this._intervalExponentialScale,
+        display: _intervalTextDisplay,
+        onChange: this._onChangeInterval
+      }),
+      Range({
+        label: "Buffer size:",
+        value: this.state.entries,
+        id: "perf-range-entries",
+        scale: this._entriesExponentialScale,
+        display: _entriesTextDisplay,
+        onChange: this._onChangeEntries
+      }),
+      this._renderThreads(),
+      this._renderFeatures()
+    );
+  }
+}
+
+/**
+ * Clean up the thread list string into a list of values.
+ * @param string threads, comma separated values.
+ * @return Array list of thread names
+ */
+function _threadStringToList(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 {string}
+ */
+function _intervalTextDisplay(value) {
+  return `${value} ms`;
+}
+
+/**
+ * Format the entries number for display.
+ * @param {number} value
+ * @return {string}
+ */
+function _entriesTextDisplay(value) {
+  return formatFileSize(value * PROFILE_ENTRY_SIZE);
+}
+
+module.exports = PerfSettings;
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/components/Range.js
@@ -0,0 +1,69 @@
+/* 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/. */
+"use strict";
+const { PureComponent } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const { div, input, label } = require("devtools/client/shared/vendor/react-dom-factories");
+
+class Range extends PureComponent {
+  static get propTypes() {
+    return {
+      value: PropTypes.number.isRequired,
+      label: PropTypes.string.isRequired,
+      id: PropTypes.string.isRequired,
+      scale: PropTypes.object.isRequired,
+      onChange: PropTypes.func.isRequired,
+      display: PropTypes.func.isRequired,
+    };
+  }
+
+  constructor(props) {
+    super(props);
+    this.handleInput = this.handleInput.bind(this);
+  }
+
+  handleInput(e) {
+    e.preventDefault();
+    const { scale, onChange } = this.props;
+    const frac = e.target.value / 100;
+    onChange(scale.fromFractionToSingleDigitValue(frac));
+  }
+
+  render() {
+    const { label: labelText, scale, id, value, display } = this.props;
+    return (
+      div(
+        { className: "perf-settings-row" },
+        label(
+          {
+            className: "perf-settings-label",
+            htmlFor: id
+          },
+          labelText
+        ),
+        div(
+          { className: "perf-settings-value" },
+          div(
+            { className: "perf-settings-range-input" },
+            input({
+              type: "range",
+              className: `perf-settings-range-input-el`,
+              min: "0",
+              max: "100",
+              value: scale.fromValueToFraction(value) * 100,
+              onChange: this.handleInput,
+              id,
+            })
+          ),
+          div(
+            { className: `perf-settings-range-value`},
+            display(value)
+          )
+        )
+      )
+    );
+  }
+}
+
+module.exports = Range;
--- a/devtools/client/performance-new/components/moz.build
+++ b/devtools/client/performance-new/components/moz.build
@@ -1,8 +1,10 @@
 # vim: set filetype=python:
 # 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/.
 
 DevToolsModules(
     'Perf.js',
+    'PerfSettings.js',
+    'Range.js',
 )
--- a/devtools/client/performance-new/initializer.js
+++ b/devtools/client/performance-new/initializer.js
@@ -14,20 +14,22 @@ const { require } = BrowserLoaderModule.
 const Perf = require("devtools/client/performance-new/components/Perf");
 const Services = require("Services");
 const { render, unmountComponentAtNode } = require("devtools/client/shared/vendor/react-dom");
 const { createElement } = require("devtools/client/shared/vendor/react");
 
 /**
  * Perform a simple initialization on the panel. Hook up event listeners.
  *
+ * @param toolbox - The toolbox
  * @param perfFront - The Perf actor's front. Used to start and stop recordings.
  */
-function gInit(perfFront) {
+function gInit(toolbox, perfFront) {
   const props = {
+    toolbox,
     perfFront,
     receiveProfile: profile => {
       // Open up a new tab and send a message with the profile.
       let browser = top.gBrowser;
       if (!browser) {
         // Current isn't browser window. Looking for the recent browser.
         const win = Services.wm.getMostRecentWindow("navigator:browser");
         if (!win) {
--- a/devtools/client/performance-new/moz.build
+++ b/devtools/client/performance-new/moz.build
@@ -4,14 +4,15 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DIRS += [
     'components',
 ]
 
 DevToolsModules(
     'panel.js',
+    'utils.js',
 )
 
 MOCHITEST_CHROME_MANIFESTS += ['test/chrome/chrome.ini']
 
 with Files('**'):
     BUG_COMPONENT = ('Firefox', 'Developer Tools: Performance Tools (Profiler/Timeline)')
--- a/devtools/client/performance-new/panel.js
+++ b/devtools/client/performance-new/panel.js
@@ -30,17 +30,17 @@ class PerformancePanel {
     this.panelWin.gToolbox = this.toolbox;
     this.panelWin.gTarget = this.target;
 
     const rootForm = await this.target.root;
     const perfFront = new PerfFront(this.target.client, rootForm);
 
     this.isReady = true;
     this.emit("ready");
-    this.panelWin.gInit(perfFront);
+    this.panelWin.gInit(this.toolbox, perfFront);
     return this;
   }
 
   // DevToolPanel API:
 
   get target() {
     return this.toolbox.target;
   }
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/utils.js
@@ -0,0 +1,162 @@
+/* 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/. */
+"use strict";
+
+const UNITS = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
+
+/**
+ * Linearly interpolate between values.
+ * https://en.wikipedia.org/wiki/Linear_interpolation
+ *
+ * @param {number} frac - Value ranged 0 - 1 to interpolate between the range
+ *                        start and range end.
+ * @param {number} rangeState - The value to start from.
+ * @param {number} rangeEnd - The value to interpolate to.
+ * @returns {number}
+ */
+function lerp(frac, rangeStart, rangeEnd) {
+  return (1 - frac) * rangeStart + frac * rangeEnd;
+}
+
+/**
+ * Make sure a value is clamped between a min and max value.
+ *
+ * @param {number} val - The value to clamp.
+ * @param {number} min - The minimum value.
+ * @returns {number}
+ */
+function clamp(val, min, max) {
+  return Math.max(min, Math.min(max, val));
+}
+
+/**
+ * Formats a file size.
+ * @param {number} num - The number (in bytes) to format.
+ * @returns {string} e.g. "10 B", "100 MB"
+ */
+function formatFileSize(num) {
+  if (!Number.isFinite(num)) {
+    throw new TypeError(`Expected a finite number, got ${typeof num}: ${num}`);
+  }
+
+  const neg = num < 0;
+
+  if (neg) {
+    num = -num;
+  }
+
+  if (num < 1) {
+    return (neg ? "-" : "") + num + " B";
+  }
+
+  const exponent = Math.min(
+    Math.floor(Math.log(num) / Math.log(1000)),
+    UNITS.length - 1
+  );
+  const numStr = Number((num / Math.pow(1000, exponent)).toPrecision(3));
+  const unit = UNITS[exponent];
+
+  return (neg ? "-" : "") + numStr + " " + unit;
+}
+
+/**
+ * Creates numbers that scale exponentially.
+ *
+ * @param {number} rangeStart
+ * @param {number} rangeEnd
+ */
+function makeExponentialScale(rangeStart, rangeEnd) {
+  const startExp = Math.log(rangeStart);
+  const endExp = Math.log(rangeEnd);
+  const fromFractionToValue = frac =>
+    Math.exp((1 - frac) * startExp + frac * endExp);
+  const fromValueToFraction = value =>
+    (Math.log(value) - startExp) / (endExp - startExp);
+  const fromFractionToSingleDigitValue = frac => {
+    return +fromFractionToValue(frac).toPrecision(1);
+  };
+  return {
+    // Takes a number ranged 0-1 and returns it within the range.
+    fromFractionToValue,
+    // Takes a number in the range, and returns a value between 0-1
+    fromValueToFraction,
+    // Takes a number ranged 0-1 and returns a value in the range, but with
+    // a single digit value.
+    fromFractionToSingleDigitValue,
+  };
+}
+
+/**
+ * Scale a source range to a destination range, but clamp it within the
+ * destination range.
+ * @param {number} val - The source range value to map to the destination range,
+ * @param {number} sourceRangeStart,
+ * @param {number} sourceRangeEnd,
+ * @param {number} destRangeStart,
+ * @param {number} destRangeEnd
+ */
+function scaleRangeWithClamping(
+  val,
+  sourceRangeStart,
+  sourceRangeEnd,
+  destRangeStart,
+  destRangeEnd
+) {
+  const frac = clamp(
+    (val - sourceRangeStart) / (sourceRangeEnd - sourceRangeStart),
+    0,
+    1
+  );
+  return lerp(frac, destRangeStart, destRangeEnd);
+}
+
+/**
+ * Use some heuristics to guess at the overhead of the recording settings.
+ * @param {number} interval
+ * @param {number} bufferSize
+ * @param {object} features - Map of the feature name to a boolean.
+ */
+function calculateOverhead(interval, bufferSize, features) {
+  const overheadFromSampling =
+    scaleRangeWithClamping(
+      Math.log(interval),
+      Math.log(0.05),
+      Math.log(1),
+      1,
+      0
+    ) +
+    scaleRangeWithClamping(
+      Math.log(interval),
+      Math.log(1),
+      Math.log(100),
+      0.1,
+      0
+    );
+  const overheadFromBuffersize = scaleRangeWithClamping(
+    Math.log(bufferSize),
+    Math.log(10),
+    Math.log(1000000),
+    0,
+    0.1
+  );
+  const overheadFromStackwalk = features.stackwalk ? 0.05 : 0;
+  const overheadFromJavaScrpt = features.js ? 0.05 : 0;
+  const overheadFromTaskTracer = features.tasktracer ? 0.05 : 0;
+  return clamp(
+    overheadFromSampling +
+      overheadFromBuffersize +
+      overheadFromStackwalk +
+      overheadFromJavaScrpt +
+      overheadFromTaskTracer,
+    0,
+    1
+  );
+}
+
+module.exports = {
+  formatFileSize,
+  makeExponentialScale,
+  scaleRangeWithClamping,
+  calculateOverhead
+};
--- a/devtools/client/themes/perf.css
+++ b/devtools/client/themes/perf.css
@@ -12,12 +12,203 @@
 }
 
 .devtools-button.perf-button {
   padding: 5px;
   margin: auto;
   font-size: 120%;
 }
 
+.perf-button-image {
+  vertical-align: text-top;
+  padding-inline-end: 4px;
+}
+
 .perf-additional-message {
   margin: 10px;
   margin-top: 65px;
 }
+
+.perf > * {
+  max-width: 440px;
+}
+
+.perf-description {
+  line-height: 1.4;
+}
+
+.perf-external-link {
+  margin: 0;
+  padding: 0;
+  background: none;
+  border: none;
+  color: var(--blue-60);
+  text-decoration: underline;
+  white-space: nowrap;
+  cursor: pointer;
+}
+
+/* Settings */
+
+.perf-settings {
+  width: 100%;
+  margin: 50px 0 25px;
+}
+
+.perf-settings-title {
+  padding: 5px 10px;
+  margin-bottom: 15px;
+  background-color: var(--grey-10);
+  border: var(--grey-30) 1px solid;
+  font-size: 11px;
+  font-weight: normal;
+}
+
+.perf-settings-row {
+  display: flex;
+  overflow: hidden;
+  line-height: 1.8;
+}
+
+.perf-settings-controls > .tree {
+  height: 100%;
+}
+
+.perf-settings-row.focused {
+  background-color: var(--theme-selection-background);
+  color: var(--theme-selection-color);
+}
+
+.perf-settings-label {
+  height: 30px;
+  min-width: 110px;
+}
+
+.perf-settings-value {
+  display: flex;
+  flex: 1;
+}
+
+.perf-settings-range-input {
+  flex: 1;
+}
+
+.perf-settings-range-input-el {
+  width: 100%;
+}
+
+.perf-settings-range-value {
+  min-width: 70px;
+  text-align: end;
+}
+
+.perf-settings-notches {
+  height: 14px;
+  margin: 5px 0 10px;
+  margin-inline-start: 0.7em;
+  display: flex;
+}
+
+.perf-settings-notch {
+  margin-right: 1px;
+  flex: 1;
+  border: 1px solid rgba(0,0,0,0.2);
+  border-radius: 2px;
+}
+
+.perf-settings-notch-normal.perf-settings-notch-active {
+  border-color: hsl(90, 90%, 40%);
+  background-color: hsla(90, 90%, 40%, 0.5);
+}
+
+.perf-settings-notch-warning.perf-settings-notch-active {
+  border-color: hsl(45, 100%, 49%);
+  background-color: hsla(45, 100%, 49%, 0.5);
+}
+
+.perf-settings-notch-critical.perf-settings-notch-active {
+  border-color: hsl(0, 90%, 40%);
+  background-color: hsla(0, 90%, 40%, 0.5);
+}
+
+.perf-settings-text-input {
+  width: 100%;
+  padding: 4px;
+  box-sizing: border-box;
+}
+
+.perf-settings-text-label {
+  flex: 1;
+}
+
+.perf-settings-details-contents {
+  overflow: hidden;
+}
+
+.perf-settings-details-contents-slider {
+  padding: 10px;
+  margin: 0 0 18px;
+  border: var(--grey-20) 1px solid;
+  background-color: var(--grey-10);
+  opacity: 0;
+  transform: translateY(-100px);
+  transition-duration: 250ms;
+  transition-timing-function: cubic-bezier(.07,.95,0,1);
+  transition-property: transform, opacity;
+}
+
+.perf-settings-details[open] .perf-settings-details-contents-slider {
+  opacity: 1;
+  transform: translateY(0);
+}
+
+.perf-settings-summary {
+  height: 30px;
+  cursor: default;
+  -moz-user-select: none;
+}
+
+.perf-settings-thread-columns {
+  margin-bottom: 20px;
+  display: flex;
+  line-height: 2;
+}
+
+.perf-settings-thread-column {
+  flex: 1;
+}
+
+.perf-settings-checkbox-label {
+  display: block;
+}
+
+.perf-settings-feature-label {
+  margin: 16px 0;
+  display: flex;
+}
+
+.perf-settings-feature-label {
+  margin: 16px 0;
+  display: flex;
+}
+
+.perf-settings-checkbox {
+  align-self: flex-start;
+}
+
+.perf-settings-feature-name {
+  width: 150px;
+  color: var(--blue-60);
+}
+
+.perf-settings-feature-title {
+  flex: 1;
+}
+
+.perf-settings-feature-name {
+  width: 130px;
+  color: var(--blue-60);
+  line-height: 1.6;
+}
+
+.perf-settings-subtext {
+  font-weight: bold;
+}